Compare commits
178 Commits
Author | SHA1 | Date | |
---|---|---|---|
ee2b3c3d10 | |||
e5819a260b | |||
a3ecf48702 | |||
1c0b904cd2 | |||
072d8a1728 | |||
964e541c32 | |||
78fec4ed22 | |||
ef111d36c9 | |||
4f64193e85 | |||
89bb6d1268 | |||
9f4226bf0f | |||
a87c2a3374 | |||
d7294ba5a0 | |||
82d286dc6f | |||
1fa18ab997 | |||
afc90f32c9 | |||
e9cfb7c21e | |||
1af8ea3769 | |||
9f7af190f1 | |||
9c703fe94d | |||
a7a11a4f13 | |||
c32c3bb62b | |||
e29d1480a6 | |||
8f299d7791 | |||
65fb2e992e | |||
41f5d677d5 | |||
a2b78b8cd9 | |||
c93f217033 | |||
82c47b6e9a | |||
94fb738c67 | |||
89071e40fc | |||
95a90c410e | |||
59fc371cd5 | |||
9b404e330d | |||
1667f9b2ef | |||
caadfc8641 | |||
bffc2e70c1 | |||
8b686f0b12 | |||
def8d1e0cb | |||
ca28c34be0 | |||
196bc3ea00 | |||
b15267be4d | |||
5c074f6f5f | |||
04cba61888 | |||
a41e2e1ceb | |||
d1d03c98ba | |||
679942159e | |||
3e48a54ab5 | |||
f6e389ff62 | |||
63c309bd12 | |||
561ec57cc8 | |||
3cefd7bd1e | |||
c63feb488c | |||
12c418d84d | |||
4b982f815c | |||
d4d3346b6d | |||
6010a103e0 | |||
5dc1da2af0 | |||
f2ccc4d963 | |||
a92d48efdd | |||
b633206b45 | |||
b6f3d2af5e | |||
5fd77d9fcc | |||
5ca4494eed | |||
de7e419ef4 | |||
20a6b3fc33 | |||
540414d8f5 | |||
abcdb8ced0 | |||
5076d73695 | |||
d88735f84e | |||
2244f0ab76 | |||
40c85d6104 | |||
42892e24f4 | |||
e6357d2ac8 | |||
1eecd85ceb | |||
c27557826b | |||
88150b6535 | |||
d63176da19 | |||
887da5aa9a | |||
6e7f1151bc | |||
b2aebcc5d3 | |||
ae9ad0fa65 | |||
fb6d852827 | |||
ba17612461 | |||
a15c7a0213 | |||
7e321d4016 | |||
a05cd5678b | |||
2ccf007b9a | |||
895b8c2c80 | |||
0f175174f6 | |||
493466683c | |||
761c342c51 | |||
5341da28d9 | |||
7768f41849 | |||
fa8993191e | |||
239ce28575 | |||
c52a49f747 | |||
e4b9895ba7 | |||
92a2bb4d32 | |||
bfec722312 | |||
5a3f7b5b70 | |||
2aa097be46 | |||
8a646d85c6 | |||
890b3eaa00 | |||
cda28ebf15 | |||
2245027ca3 | |||
3dc250f801 | |||
66e786a1b0 | |||
1e26926350 | |||
8bd7ea5bbc | |||
6eb36abe2e | |||
6fced3fab2 | |||
774e456e54 | |||
519859e1c5 | |||
26f0c488e5 | |||
1b0b53fbd0 | |||
35f4ea29f9 | |||
fff39d9879 | |||
444e761d41 | |||
68a3def35a | |||
d9426d301d | |||
8bcf7109a3 | |||
3effdf0f4d | |||
3c122bcf53 | |||
037ff52f4f | |||
b11f8acba1 | |||
ef9a633aa4 | |||
c7e2f979dd | |||
e97bb9c933 | |||
fa506b5bf8 | |||
e3193a92d0 | |||
d76dabdca6 | |||
d219f50912 | |||
e08710a19c | |||
4e167b35be | |||
16873384a8 | |||
f87339f9fa | |||
5f5e5e3211 | |||
f724db8226 | |||
c7e90cd7df | |||
8c5b00b1a3 | |||
a3b79fbcd8 | |||
9db77e6351 | |||
5bc1eaec9f | |||
81c9ce7284 | |||
caa6978d80 | |||
af22d6a4e3 | |||
0eabb3c37c | |||
2b84791391 | |||
6f896cb096 | |||
9a488c60f2 | |||
9cb50446f4 | |||
8c00a2359e | |||
d1ff34d16d | |||
8e8615dab8 | |||
8b71556425 | |||
ae6e1bfd85 | |||
6d1f3b73ef | |||
0dcaf80c7f | |||
fc9cd5bdf0 | |||
a434c45196 | |||
30a3a84ec9 | |||
cfaa5766ed | |||
8b08db308b | |||
3e2ff55954 | |||
a34d1641b3 | |||
40c645e433 | |||
b2e5415a35 | |||
365ee4cf0b | |||
2b4603a234 | |||
ec23eae21d | |||
7a9229628a | |||
27bde55f54 | |||
b5d360594a | |||
e3833914b3 | |||
49cdec6961 | |||
7fa1b65af0 | |||
c00c95efcf |
@ -34,6 +34,7 @@ using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -121,7 +122,7 @@ namespace BTCPayServer.Tests
|
||||
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
|
||||
|
||||
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath });
|
||||
var conf = new DefaultConfiguration() { Logger = Logs.LogProvider.CreateLogger("Console") }.CreateConfiguration(new[] { "--datadir", _Directory, "--conf", confPath, "--disable-registration", "false" });
|
||||
_Host = new WebHostBuilder()
|
||||
.UseConfiguration(conf)
|
||||
.ConfigureServices(s =>
|
||||
|
89
BTCPayServer.Tests/CoinSwitchTests.cs
Normal file
89
BTCPayServer.Tests/CoinSwitchTests.cs
Normal file
@ -0,0 +1,89 @@
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CoinSwitchTests
|
||||
{
|
||||
public CoinSwitchTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanSetCoinSwitchPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
|
||||
var storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.Null(storeBlob.CoinSwitchSettings);
|
||||
|
||||
var updateModel = new UpdateCoinSwitchSettingsViewModel()
|
||||
{
|
||||
MerchantId = "aaa",
|
||||
};
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
storeBlob = controller.StoreData.GetStoreBlob();
|
||||
Assert.NotNull(storeBlob.CoinSwitchSettings);
|
||||
Assert.NotNull(storeBlob.CoinSwitchSettings);
|
||||
Assert.IsType<CoinSwitchSettings>(storeBlob.CoinSwitchSettings);
|
||||
Assert.Equal(storeBlob.CoinSwitchSettings.MerchantId,
|
||||
updateModel.MerchantId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async void CanToggleCoinSwitchPaymentMethod()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var controller = tester.PayTester.GetController<StoresController>(user.UserId, user.StoreId);
|
||||
|
||||
var updateModel = new UpdateCoinSwitchSettingsViewModel()
|
||||
{
|
||||
MerchantId = "aaa",
|
||||
Enabled = true
|
||||
};
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
|
||||
var store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.True(store.GetStoreBlob().CoinSwitchSettings.Enabled);
|
||||
|
||||
updateModel.Enabled = false;
|
||||
|
||||
Assert.Equal("UpdateStore", Assert.IsType<RedirectToActionResult>(
|
||||
await controller.UpdateCoinSwitchSettings(user.StoreId, updateModel, "save")).ActionName);
|
||||
|
||||
store = await tester.PayTester.StoreRepository.FindStore(user.StoreId);
|
||||
|
||||
Assert.False(store.GetStoreBlob().CoinSwitchSettings.Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
251
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
251
BTCPayServer.Tests/CrowdfundTests.cs
Normal file
@ -0,0 +1,251 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Hubs;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Changelly.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CrowdfundTests
|
||||
{
|
||||
public CrowdfundTests(ITestOutputHelper helper)
|
||||
{
|
||||
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
|
||||
Logs.LogProvider = new XUnitLogProvider(helper);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanCreateAndDeleteCrowdfundApp()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
var user2 = tester.NewAccount();
|
||||
user2.GrantAccess();
|
||||
var apps = user.GetController<AppsController>();
|
||||
var apps2 = user2.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
Assert.NotNull(vm.SelectedAppType);
|
||||
Assert.Null(vm.Name);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
Assert.Equal(nameof(apps.UpdateCrowdfund), redirectToAction.ActionName);
|
||||
var appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
var appList2 =
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps().Result).Model);
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(appList.Apps[0].IsOwner);
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id).Result);
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id).Result);
|
||||
redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal(nameof(apps.ListApps), redirectToAction.ActionName);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanContributeOnlyWhenAllowed()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
|
||||
.Apps[0].Id;
|
||||
|
||||
//Scenario 1: Not Enabled - Not Allowed
|
||||
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.Enabled = false;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
|
||||
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
//Scenario 2: Not Enabled But Admin - Allowed
|
||||
Assert.IsType<OkObjectResult>(await publicApps.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
RedirectToCheckout = false,
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
Assert.IsType<ViewResult>(await publicApps.ViewCrowdfund(appId, string.Empty));
|
||||
Assert.IsType<NotFoundResult>(await anonAppPubsController.ViewCrowdfund(appId, string.Empty));
|
||||
|
||||
//Scenario 3: Enabled But Start Date > Now - Not Allowed
|
||||
crowdfundViewModel.StartDate= DateTime.Today.AddDays(2);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
|
||||
//Scenario 4: Enabled But End Date < Now - Not Allowed
|
||||
|
||||
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
|
||||
crowdfundViewModel.EndDate= DateTime.Today.AddDays(-1);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.01)
|
||||
}));
|
||||
|
||||
|
||||
//Scenario 5: Enabled and within correct timeframe, however target is enforced and Amount is Over - Not Allowed
|
||||
crowdfundViewModel.StartDate= DateTime.Today.AddDays(-2);
|
||||
crowdfundViewModel.EndDate= DateTime.Today.AddDays(2);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.TargetAmount = 1;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.EnforceTargetAmount = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
Assert.IsType<NotFoundObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(1.01)
|
||||
}));
|
||||
|
||||
//Scenario 6: Allowed
|
||||
Assert.IsType<OkObjectResult>(await anonAppPubsController.ContributeToCrowdfund(appId, new ContributeToCrowdfund()
|
||||
{
|
||||
Amount = new decimal(0.05)
|
||||
}));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanComputeCrowdfundModel()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var apps = user.GetController<AppsController>();
|
||||
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
|
||||
vm.Name = "test";
|
||||
vm.SelectedAppType = AppType.Crowdfund.ToString();
|
||||
Assert.IsType<RedirectToActionResult>(apps.CreateApp(vm).Result);
|
||||
var appId = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps().Result).Model)
|
||||
.Apps[0].Id;
|
||||
|
||||
var crowdfundViewModel = Assert.IsType<UpdateCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(apps.UpdateCrowdfund(appId).Result).Model);
|
||||
crowdfundViewModel.Enabled = true;
|
||||
crowdfundViewModel.EndDate = null;
|
||||
crowdfundViewModel.TargetAmount = 100;
|
||||
crowdfundViewModel.TargetCurrency = "BTC";
|
||||
crowdfundViewModel.UseAllStoreInvoices = true;
|
||||
Assert.IsType<RedirectToActionResult>(apps.UpdateCrowdfund(appId, crowdfundViewModel).Result);
|
||||
|
||||
var anonAppPubsController = tester.PayTester.GetController<AppsPublicController>();
|
||||
var publicApps = user.GetController<AppsPublicController>();
|
||||
|
||||
var model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
|
||||
Assert.Equal(crowdfundViewModel.TargetAmount, model.TargetAmount );
|
||||
Assert.Equal(crowdfundViewModel.EndDate, model.EndDate );
|
||||
Assert.Equal(crowdfundViewModel.StartDate, model.StartDate );
|
||||
Assert.Equal(crowdfundViewModel.TargetCurrency, model.TargetCurrency );
|
||||
Assert.Equal(0m, model.Info.CurrentAmount );
|
||||
Assert.Equal(0m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal(0m, model.Info.ProgressPercentage);
|
||||
|
||||
|
||||
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Buyer = new Buyer() { email = "test@fwf.com" },
|
||||
Price = 1m,
|
||||
Currency = "BTC",
|
||||
PosData = "posData",
|
||||
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
|
||||
ItemDesc = "Some description",
|
||||
TransactionSpeed = "high",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
|
||||
model = Assert.IsType<ViewCrowdfundViewModel>(Assert
|
||||
.IsType<ViewResult>(publicApps.ViewCrowdfund(appId, String.Empty).Result).Model);
|
||||
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, tester.ExplorerNode.Network);
|
||||
|
||||
|
||||
|
||||
tester.ExplorerNode.SendToAddress(invoiceAddress,invoice.BtcDue);
|
||||
Assert.Equal(0m ,model.Info.CurrentAmount );
|
||||
Assert.Equal(1m, model.Info.CurrentPendingAmount);
|
||||
Assert.Equal( 0m, model.Info.ProgressPercentage);
|
||||
Assert.Equal(1m, model.Info.PendingProgressPercentage);
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -8,30 +8,27 @@ using Microsoft.AspNetCore.Builder;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using System.Threading.Channels;
|
||||
using System.IO;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
public class CustomServer : IDisposable
|
||||
{
|
||||
TaskCompletionSource<bool> _Evt = null;
|
||||
IWebHost _Host = null;
|
||||
CancellationTokenSource _Closed = new CancellationTokenSource();
|
||||
Channel<JObject> _Requests = Channel.CreateUnbounded<JObject>();
|
||||
public CustomServer()
|
||||
{
|
||||
{
|
||||
var port = Utils.FreeTcpPort();
|
||||
_Host = new WebHostBuilder()
|
||||
.Configure(app =>
|
||||
{
|
||||
app.Run(req =>
|
||||
{
|
||||
while (_Act == null)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
_Closed.Token.ThrowIfCancellationRequested();
|
||||
}
|
||||
_Act(req);
|
||||
_Act = null;
|
||||
_Evt.TrySetResult(true);
|
||||
_Requests.Writer.WriteAsync(JsonConvert.DeserializeObject<JObject>(new StreamReader(req.Request.Body).ReadToEnd()), _Closed.Token);
|
||||
req.Response.StatusCode = 200;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
@ -47,22 +44,24 @@ namespace BTCPayServer.Tests
|
||||
return new Uri(_Host.ServerFeatures.Get<IServerAddressesFeature>().Addresses.First());
|
||||
}
|
||||
|
||||
Action<HttpContext> _Act;
|
||||
public void ProcessNextRequest(Action<HttpContext> act)
|
||||
public async Task<JObject> GetNextRequest()
|
||||
{
|
||||
var source = new TaskCompletionSource<bool>();
|
||||
CancellationTokenSource cancellation = new CancellationTokenSource(20000);
|
||||
cancellation.Token.Register(() => source.TrySetCanceled());
|
||||
source = new TaskCompletionSource<bool>();
|
||||
_Evt = source;
|
||||
_Act = act;
|
||||
try
|
||||
using (CancellationTokenSource cancellation = new CancellationTokenSource(2000000))
|
||||
{
|
||||
_Evt.Task.GetAwaiter().GetResult();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
||||
try
|
||||
{
|
||||
JObject req = null;
|
||||
while(!await _Requests.Reader.WaitToReadAsync(cancellation.Token) ||
|
||||
!_Requests.Reader.TryRead(out req))
|
||||
{
|
||||
|
||||
}
|
||||
return req;
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
throw new Xunit.Sdk.XunitException("Callback to the webserver was expected, check if the callback url is accessible from internet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ using BTCPayServer.Tests.Lnd;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -152,6 +153,7 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<string> Stores { get; internal set; } = new List<string>();
|
||||
|
||||
public void Dispose()
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.CLightning;
|
||||
using BTCPayServer.Data;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -58,6 +59,21 @@ namespace BTCPayServer.Tests
|
||||
CreateStoreAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void SetNetworkFeeMode(NetworkFeeMode mode)
|
||||
{
|
||||
ModifyStore((store) =>
|
||||
{
|
||||
store.NetworkFeeMode = mode;
|
||||
});
|
||||
}
|
||||
public void ModifyStore(Action<StoreViewModel> modify)
|
||||
{
|
||||
var storeController = GetController<StoresController>();
|
||||
StoreViewModel store = (StoreViewModel)((ViewResult)storeController.UpdateStore()).Model;
|
||||
modify(store);
|
||||
storeController.UpdateStore(store).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public T GetController<T>(bool setImplicitStore = true) where T : Controller
|
||||
{
|
||||
return parent.PayTester.GetController<T>(UserId, setImplicitStore ? StoreId : null);
|
||||
@ -83,10 +99,6 @@ namespace BTCPayServer.Tests
|
||||
var store = parent.PayTester.GetController<StoresController>(UserId, StoreId);
|
||||
ExtKey = new ExtKey().GetWif(SupportedNetwork.NBitcoinNetwork);
|
||||
DerivationScheme = new DerivationStrategyFactory(SupportedNetwork.NBitcoinNetwork).Parse(ExtKey.Neuter().ToString() + (segwit ? "" : "-[legacy]"));
|
||||
var vm = (StoreViewModel)((ViewResult)store.UpdateStore()).Model;
|
||||
vm.SpeedPolicy = SpeedPolicy.MediumSpeed;
|
||||
await store.UpdateStore(vm);
|
||||
|
||||
await store.AddDerivationScheme(StoreId, new DerivationSchemeViewModel()
|
||||
{
|
||||
DerivationScheme = DerivationScheme.ToString(),
|
||||
|
@ -49,6 +49,9 @@ using BTCPayServer.Security;
|
||||
using NBXplorer.Models;
|
||||
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
|
||||
using NBitpayClient.Extensions;
|
||||
using BTCPayServer.Services;
|
||||
using System.Text.RegularExpressions;
|
||||
using BTCPayServer.Events;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -98,7 +101,7 @@ namespace BTCPayServer.Tests
|
||||
Rate = 10513.44m,
|
||||
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
TxFee = Money.Coins(0.00000100m),
|
||||
NextNetworkFee = Money.Coins(0.00000100m),
|
||||
DepositAddress = dummy
|
||||
}));
|
||||
paymentMethods.Add(new PaymentMethod()
|
||||
@ -107,7 +110,7 @@ namespace BTCPayServer.Tests
|
||||
Rate = 216.79m
|
||||
}.SetPaymentMethodDetails(new BTCPayServer.Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod()
|
||||
{
|
||||
TxFee = Money.Coins(0.00010000m),
|
||||
NextNetworkFee = Money.Coins(0.00010000m),
|
||||
DepositAddress = dummy
|
||||
}));
|
||||
invoiceEntity.SetPaymentMethods(paymentMethods);
|
||||
@ -115,12 +118,12 @@ namespace BTCPayServer.Tests
|
||||
var btc = invoiceEntity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
var accounting = btc.Calculate();
|
||||
|
||||
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m }.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||
{
|
||||
Output = new TxOut() { Value = Money.Coins(0.00151263m) }
|
||||
}));
|
||||
accounting = btc.Calculate();
|
||||
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC" }.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||
invoiceEntity.Payments.Add(new PaymentEntity() { Accounted = true, CryptoCode = "BTC", NetworkFee = 0.00000100m }.SetCryptoPaymentData(new BitcoinLikePaymentData()
|
||||
{
|
||||
Output = new TxOut() { Value = accounting.Due }
|
||||
}));
|
||||
@ -147,7 +150,7 @@ namespace BTCPayServer.Tests
|
||||
var entity = new InvoiceEntity();
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new System.Collections.Generic.List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
|
||||
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
|
||||
var paymentMethod = entity.GetPaymentMethods(null).TryGet("BTC", PaymentTypes.BTCLike);
|
||||
@ -155,20 +158,20 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.1m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true });
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.5m), new Key()), Accounted = true, NetworkFee = 0.1m });
|
||||
|
||||
accounting = paymentMethod.Calculate();
|
||||
//Since we need to spend one more txout, it should be 1.1 - 0,5 + 0.1
|
||||
Assert.Equal(Money.Coins(0.7m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.2m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true });
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.2m), new Key()), Accounted = true, NetworkFee = 0.1m });
|
||||
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(0.6m), accounting.Due);
|
||||
Assert.Equal(Money.Coins(1.3m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true });
|
||||
entity.Payments.Add(new PaymentEntity() { Output = new TxOut(Money.Coins(0.6m), new Key()), Accounted = true, NetworkFee = 0.1m });
|
||||
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Zero, accounting.Due);
|
||||
@ -187,13 +190,13 @@ namespace BTCPayServer.Tests
|
||||
{
|
||||
CryptoCode = "BTC",
|
||||
Rate = 1000,
|
||||
TxFee = Money.Coins(0.1m)
|
||||
NextNetworkFee = Money.Coins(0.1m)
|
||||
});
|
||||
paymentMethods.Add(new PaymentMethod()
|
||||
{
|
||||
CryptoCode = "LTC",
|
||||
Rate = 500,
|
||||
TxFee = Money.Coins(0.01m)
|
||||
NextNetworkFee = Money.Coins(0.01m)
|
||||
});
|
||||
entity.SetPaymentMethods(paymentMethods);
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
@ -205,7 +208,7 @@ namespace BTCPayServer.Tests
|
||||
accounting = paymentMethod.Calculate();
|
||||
Assert.Equal(Money.Coins(10.01m), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.1m });
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
@ -222,7 +225,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(Money.Coins(2.0m), accounting.Paid);
|
||||
Assert.Equal(Money.Coins(10.01m + 0.1m * 2), accounting.TotalDue);
|
||||
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true });
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "LTC", Output = new TxOut(Money.Coins(1.0m), new Key()), Accounted = true, NetworkFee = 0.01m });
|
||||
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
@ -242,7 +245,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(2, accounting.TxRequired);
|
||||
|
||||
var remaining = Money.Coins(4.2m - 0.5m + 0.01m / 2);
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true });
|
||||
entity.Payments.Add(new PaymentEntity() { CryptoCode = "BTC", Output = new TxOut(remaining, new Key()), Accounted = true, NetworkFee = 0.1m });
|
||||
|
||||
paymentMethod = entity.GetPaymentMethod(new PaymentMethodId("BTC", PaymentTypes.BTCLike), null);
|
||||
accounting = paymentMethod.Calculate();
|
||||
@ -272,7 +275,7 @@ namespace BTCPayServer.Tests
|
||||
var entity = new InvoiceEntity();
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = new List<PaymentEntity>();
|
||||
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, TxFee = Money.Coins(0.1m) });
|
||||
entity.SetPaymentMethod(new PaymentMethod() { CryptoCode = "BTC", Rate = 5000, NextNetworkFee = Money.Coins(0.1m) });
|
||||
entity.ProductInformation = new ProductInformation() { Price = 5000 };
|
||||
entity.PaymentTolerance = 0;
|
||||
|
||||
@ -326,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);
|
||||
@ -349,7 +352,7 @@ namespace BTCPayServer.Tests
|
||||
(1000.0001m, "₹ 1,000.00 (INR)", "INR")
|
||||
})
|
||||
{
|
||||
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3);
|
||||
var actual = new CurrencyNameTable().DisplayFormatCurrency(test.Item1, test.Item3);
|
||||
actual = actual.Replace("¥", "¥"); // Hack so JPY test pass on linux as well
|
||||
Assert.Equal(test.Item2, actual);
|
||||
}
|
||||
@ -448,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);
|
||||
@ -484,7 +487,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanSendIPN()
|
||||
public async Task CanSendIPN()
|
||||
{
|
||||
using (var callbackServer = new CustomServer())
|
||||
{
|
||||
@ -494,6 +497,7 @@ namespace BTCPayServer.Tests
|
||||
var acc = tester.NewAccount();
|
||||
acc.GrantAccess();
|
||||
acc.RegisterDerivationScheme("BTC");
|
||||
acc.ModifyStore(s => s.SpeedPolicy = SpeedPolicy.LowSpeed);
|
||||
var invoice = acc.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5.0m,
|
||||
@ -506,13 +510,43 @@ namespace BTCPayServer.Tests
|
||||
ExtendedNotifications = true
|
||||
});
|
||||
BitcoinUrlBuilder url = new BitcoinUrlBuilder(invoice.PaymentUrls.BIP21);
|
||||
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
|
||||
Thread.Sleep(5000);
|
||||
callbackServer.ProcessNextRequest((ctx) =>
|
||||
bool receivedPayment = false;
|
||||
bool paid = false;
|
||||
bool confirmed = false;
|
||||
bool completed = false;
|
||||
while (!completed || !confirmed)
|
||||
{
|
||||
var ipn = new StreamReader(ctx.Request.Body).ReadToEnd();
|
||||
JsonConvert.DeserializeObject<InvoicePaymentNotification>(ipn); //can deserialize
|
||||
});
|
||||
var request = await callbackServer.GetNextRequest();
|
||||
if (request.ContainsKey("event"))
|
||||
{
|
||||
var evtName = request["event"]["name"].Value<string>();
|
||||
switch (evtName)
|
||||
{
|
||||
case InvoiceEvent.Created:
|
||||
tester.ExplorerNode.SendToAddress(url.Address, url.Amount);
|
||||
break;
|
||||
case InvoiceEvent.ReceivedPayment:
|
||||
receivedPayment = true;
|
||||
break;
|
||||
case InvoiceEvent.PaidInFull:
|
||||
Assert.True(receivedPayment);
|
||||
tester.ExplorerNode.Generate(6);
|
||||
paid = true;
|
||||
break;
|
||||
case InvoiceEvent.Confirmed:
|
||||
Assert.True(paid);
|
||||
confirmed = true;
|
||||
break;
|
||||
case InvoiceEvent.Completed:
|
||||
Assert.True(paid); //TODO: Fix, out of order event mean we can receive invoice_confirmed after invoice_complete
|
||||
completed = true;
|
||||
break;
|
||||
default:
|
||||
Assert.False(true, $"{evtName} was not expected");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var invoice2 = acc.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.NotNull(invoice2);
|
||||
}
|
||||
@ -651,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);
|
||||
@ -726,6 +760,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 5000.0m,
|
||||
@ -747,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);
|
||||
@ -776,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);
|
||||
@ -1023,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);
|
||||
@ -1044,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);
|
||||
@ -1142,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);
|
||||
@ -1184,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);
|
||||
@ -1198,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);
|
||||
@ -1429,6 +1464,7 @@ namespace BTCPayServer.Tests
|
||||
vmpos.ButtonText = "{0} Purchase";
|
||||
vmpos.CustomButtonText = "Nicolas Sexy Hair";
|
||||
vmpos.CustomTipText = "Wanna tip?";
|
||||
vmpos.CustomTipPercentages = "15,18,20";
|
||||
vmpos.Template = @"
|
||||
apple:
|
||||
price: 5.0
|
||||
@ -1454,6 +1490,7 @@ donation:
|
||||
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
||||
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
|
||||
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
||||
|
||||
//
|
||||
@ -1589,16 +1626,22 @@ donation:
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanExportInvoicesJson()
|
||||
{
|
||||
decimal GetFieldValue(string input, string fieldName)
|
||||
{
|
||||
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
|
||||
Assert.True(match.Success);
|
||||
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
|
||||
}
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 500,
|
||||
Price = 10,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
@ -1606,6 +1649,7 @@ donation:
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
|
||||
// ensure 0 invoices exported because there are no payments yet
|
||||
var jsonResult = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
|
||||
var result = Assert.IsType<ContentResult>(jsonResult);
|
||||
@ -1614,46 +1658,123 @@ 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 - 3 * networkFee;
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Thread.Sleep(1000); // prevent race conditions, ordering payments
|
||||
// look if you can reduce thread sleep, this was min value for me
|
||||
|
||||
Eventually(() =>
|
||||
// should reduce invoice due by 0 USD because payment = network fee
|
||||
cashCow.SendToAddress(invoiceAddress, networkFee);
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// pay remaining amount
|
||||
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
|
||||
Thread.Sleep(1000);
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var jsonResultPaid = user.GetController<InvoiceController>().Export("json").GetAwaiter().GetResult();
|
||||
var paidresult = Assert.IsType<ContentResult>(jsonResultPaid);
|
||||
Assert.Equal("application/json", paidresult.ContentType);
|
||||
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", paidresult.Content);
|
||||
Assert.Contains("\"InvoicePrice\": 500.0", paidresult.Content);
|
||||
Assert.Contains("\"ConversionRate\": 5000.0", paidresult.Content);
|
||||
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", paidresult.Content);
|
||||
});
|
||||
|
||||
/*
|
||||
[
|
||||
{
|
||||
"ReceivedDate": "2018-11-30T10:27:13Z",
|
||||
"StoreId": "FKaSZrXLJ2tcLfCyeiYYfmZp1UM5nZ1LDecQqbwBRuHi",
|
||||
"OrderId": "orderId",
|
||||
"InvoiceId": "4XUkgPMaTBzwJGV9P84kPC",
|
||||
"CreatedDate": "2018-11-30T10:27:06Z",
|
||||
"ExpirationDate": "2018-11-30T10:42:06Z",
|
||||
"MonitoringDate": "2018-11-30T11:42:06Z",
|
||||
"PaymentId": "6e5755c3357b20fd66f5fc478778d81371eab341e7112ab66ed6122c0ec0d9e5-1",
|
||||
"CryptoCode": "BTC",
|
||||
"Destination": "mhhSEQuoM993o6vwnBeufJ4TaWov2ZUsPQ",
|
||||
"PaymentType": "OnChain",
|
||||
"PaymentDue": "0.10020000 BTC",
|
||||
"PaymentPaid": "0.10009990 BTC",
|
||||
"PaymentOverpaid": "0.00000000 BTC",
|
||||
"ConversionRate": 5000.0,
|
||||
"FiatPrice": 500.0,
|
||||
"FiatCurrency": "USD",
|
||||
"ItemCode": null,
|
||||
"ItemDesc": "Some \", description",
|
||||
"Status": "new"
|
||||
}
|
||||
]
|
||||
*/
|
||||
var parsedJson = JsonConvert.DeserializeObject<object[]>(paidresult.Content);
|
||||
Assert.Equal(3, parsedJson.Length);
|
||||
|
||||
var invoiceDueAfterFirstPayment = (3 * networkFee).ToDecimal(MoneyUnit.BTC) * invoice.Rate;
|
||||
var pay1str = parsedJson[0].ToString();
|
||||
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
|
||||
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
|
||||
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
|
||||
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
|
||||
|
||||
var pay2str = parsedJson[1].ToString();
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
|
||||
|
||||
var pay3str = parsedJson[2].ToString();
|
||||
Assert.Contains("\"InvoiceDue\": 0", pay3str);
|
||||
});
|
||||
}
|
||||
}
|
||||
[Fact]
|
||||
[Trait("Integration", "Integration")]
|
||||
public void CanChangeNetworkFeeMode()
|
||||
{
|
||||
using (var tester = ServerTester.Create())
|
||||
{
|
||||
tester.Start();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
foreach (var networkFeeMode in Enum.GetValues(typeof(NetworkFeeMode)).Cast<NetworkFeeMode>())
|
||||
{
|
||||
Logs.Tester.LogInformation($"Trying with {nameof(networkFeeMode)}={networkFeeMode}");
|
||||
user.SetNetworkFeeMode(networkFeeMode);
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 10,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var networkFee = Money.Satoshis(10000).ToDecimal(MoneyUnit.BTC);
|
||||
var missingMoney = Money.Satoshis(5000).ToDecimal(MoneyUnit.BTC);
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
|
||||
// Check that for the first payment, no network fee are included
|
||||
var due = Money.Parse(invoice.CryptoInfo[0].Due);
|
||||
var productPartDue = (invoice.Price / invoice.Rate);
|
||||
switch (networkFeeMode)
|
||||
{
|
||||
case NetworkFeeMode.MultiplePaymentsOnly:
|
||||
case NetworkFeeMode.Never:
|
||||
Assert.Equal(productPartDue, due.ToDecimal(MoneyUnit.BTC));
|
||||
break;
|
||||
case NetworkFeeMode.Always:
|
||||
Assert.Equal(productPartDue + networkFee, due.ToDecimal(MoneyUnit.BTC));
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(networkFeeMode.ToString());
|
||||
}
|
||||
var firstPayment = productPartDue - missingMoney;
|
||||
cashCow.SendToAddress(invoiceAddress, Money.Coins(firstPayment));
|
||||
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
// Check that for the second payment, network fee are included
|
||||
due = Money.Parse(invoice.CryptoInfo[0].Due);
|
||||
Assert.Equal(Money.Coins(firstPayment), Money.Parse(invoice.CryptoInfo[0].Paid));
|
||||
switch (networkFeeMode)
|
||||
{
|
||||
case NetworkFeeMode.MultiplePaymentsOnly:
|
||||
Assert.Equal(missingMoney + networkFee, due.ToDecimal(MoneyUnit.BTC));
|
||||
Assert.Equal(firstPayment + missingMoney + networkFee, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
|
||||
break;
|
||||
case NetworkFeeMode.Always:
|
||||
Assert.Equal(missingMoney + 2 * networkFee, due.ToDecimal(MoneyUnit.BTC));
|
||||
Assert.Equal(firstPayment + missingMoney + 2 * networkFee, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
|
||||
break;
|
||||
case NetworkFeeMode.Never:
|
||||
Assert.Equal(missingMoney, due.ToDecimal(MoneyUnit.BTC));
|
||||
Assert.Equal(firstPayment + missingMoney, Money.Parse(invoice.CryptoInfo[0].TotalDue).ToDecimal(MoneyUnit.BTC));
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException(networkFeeMode.ToString());
|
||||
}
|
||||
});
|
||||
cashCow.SendToAddress(invoiceAddress, due);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
invoice = user.BitPay.GetInvoice(invoice.Id);
|
||||
Assert.Equal("paid", invoice.Status);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1667,7 +1788,7 @@ donation:
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = user.BitPay.CreateInvoice(new Invoice()
|
||||
{
|
||||
Price = 500,
|
||||
@ -1680,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\",\"0.1000999\",\"BTC\",\"5000.0\",\"500.0\"", paidresult.Content);
|
||||
Assert.Contains($",\"USD\",\"\",\"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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1758,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()
|
||||
{
|
||||
@ -1804,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);
|
||||
@ -1827,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);
|
||||
@ -1841,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);
|
||||
@ -1849,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);
|
||||
@ -1871,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);
|
||||
@ -1888,7 +2010,7 @@ donation:
|
||||
|
||||
cashCow.Generate(1);
|
||||
|
||||
Eventually(() =>
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var localInvoice = user.BitPay.GetInvoice(invoice.Id, Facade.Merchant);
|
||||
Assert.Equal("confirmed", localInvoice.Status);
|
||||
@ -2078,36 +2200,39 @@ donation:
|
||||
return ctx.AddressInvoices.FirstOrDefault(i => i.InvoiceDataId == invoice.Id && i.GetAddress() == h) != null;
|
||||
}
|
||||
|
||||
private void Eventually(Action act)
|
||||
public static class TestUtils
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
public static void Eventually(Action act)
|
||||
{
|
||||
try
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
try
|
||||
{
|
||||
act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
cts.Token.WaitHandle.WaitOne(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
public static async Task EventuallyAsync(Func<Task> act)
|
||||
{
|
||||
try
|
||||
CancellationTokenSource cts = new CancellationTokenSource(20000);
|
||||
while (true)
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
try
|
||||
{
|
||||
await act();
|
||||
break;
|
||||
}
|
||||
catch (XunitException) when (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ version: "3"
|
||||
|
||||
# Run `docker-compose up dev` for bootstrapping your development environment
|
||||
# Doing so will expose NBXplorer, Bitcoind RPC and postgres port to the host so that tests can Run,
|
||||
# The Visual Studio launch setting `Docker-Regtest` is configured to use this environment.
|
||||
# The Visual Studio launch setting `Docker-regtest` is configured to use this environment.
|
||||
services:
|
||||
|
||||
tests:
|
||||
|
@ -2,7 +2,7 @@
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<Version>1.0.3.34</Version>
|
||||
<Version>1.0.3.41</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>
|
||||
|
@ -61,6 +61,12 @@ namespace BTCPayServer.Configuration
|
||||
set;
|
||||
} = new List<NBXplorerConnectionSetting>();
|
||||
|
||||
public bool DisableRegistration
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public static string GetDebugLog(IConfiguration configuration)
|
||||
{
|
||||
return configuration.GetValue<string>("debuglog", null);
|
||||
@ -237,6 +243,8 @@ namespace BTCPayServer.Configuration
|
||||
Logs.Configuration.LogInformation("LogFile: " + LogFile);
|
||||
Logs.Configuration.LogInformation("Log Level: " + GetDebugLogLevel(conf));
|
||||
}
|
||||
|
||||
DisableRegistration = conf.GetOrDefault<bool>("disable-registration", true);
|
||||
}
|
||||
|
||||
private SSHSettings ParseSSHConfiguration(IConfiguration conf)
|
||||
|
@ -43,6 +43,7 @@ namespace BTCPayServer.Configuration
|
||||
app.Option("--sshtrustedfingerprints", "SSH Host public key fingerprint or sha256 (default: empty, it will allow untrusted connections)", CommandOptionType.SingleValue);
|
||||
app.Option("--debuglog", "A rolling log file for debug messages.", CommandOptionType.SingleValue);
|
||||
app.Option("--debugloglevel", "The severity you log (default:information)", CommandOptionType.SingleValue);
|
||||
app.Option("--disable-registration", "Disables new user registrations (default:true)", CommandOptionType.SingleValue);
|
||||
foreach (var network in provider.GetAll())
|
||||
{
|
||||
var crypto = network.CryptoCode.ToLowerInvariant();
|
||||
|
@ -32,6 +32,7 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository storeRepository;
|
||||
RoleManager<IdentityRole> _RoleManager;
|
||||
SettingsRepository _SettingsRepository;
|
||||
Configuration.BTCPayServerOptions _Options;
|
||||
ILogger _logger;
|
||||
|
||||
public AccountController(
|
||||
@ -40,7 +41,8 @@ namespace BTCPayServer.Controllers
|
||||
StoreRepository storeRepository,
|
||||
SignInManager<ApplicationUser> signInManager,
|
||||
IEmailSender emailSender,
|
||||
SettingsRepository settingsRepository)
|
||||
SettingsRepository settingsRepository,
|
||||
Configuration.BTCPayServerOptions options)
|
||||
{
|
||||
this.storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
@ -48,6 +50,7 @@ namespace BTCPayServer.Controllers
|
||||
_emailSender = emailSender;
|
||||
_RoleManager = roleManager;
|
||||
_SettingsRepository = settingsRepository;
|
||||
_Options = options;
|
||||
_logger = Logs.PayServer;
|
||||
}
|
||||
|
||||
@ -271,6 +274,13 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
await _RoleManager.CreateAsync(new IdentityRole(Roles.ServerAdmin));
|
||||
await _userManager.AddToRoleAsync(user, Roles.ServerAdmin);
|
||||
|
||||
if(_Options.DisableRegistration)
|
||||
{
|
||||
// Once the admin user has been created lock subsequent user registrations (needs to be disabled for unit tests that require multiple users).
|
||||
policies.LockSubscription = true;
|
||||
await _SettingsRepository.UpdateSetting(policies);
|
||||
}
|
||||
}
|
||||
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
|
171
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
171
BTCPayServer/Controllers/AppsController.Crowdfund.cs
Normal file
@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class AppsController
|
||||
{
|
||||
public class CrowdfundAppUpdated
|
||||
{
|
||||
public string AppId { get; set; }
|
||||
public CrowdfundSettings Settings { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
}
|
||||
|
||||
public class CrowdfundSettings
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
public string TargetCurrency { get; set; }
|
||||
public decimal? TargetAmount { get; set; }
|
||||
|
||||
public bool EnforceTargetAmount { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string MainImageUrl { get; set; }
|
||||
public string NotificationUrl { get; set; }
|
||||
public string Tagline { get; set; }
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string PerksTemplate { get; set; }
|
||||
public bool DisqusEnabled { get; set; }= false;
|
||||
public bool SoundsEnabled { get; set; }= true;
|
||||
public string DisqusShortname { get; set; }
|
||||
public bool AnimationsEnabled { get; set; } = true;
|
||||
public bool UseInvoiceAmount { get; set; } = true;
|
||||
public int ResetEveryAmount { get; set; } = 1;
|
||||
public CrowdfundResetEvery ResetEvery { get; set; } = CrowdfundResetEvery.Never;
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
public bool SortPerksByPopularity { get; set; }
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId)
|
||||
{
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var vm = new UpdateCrowdfundViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Enabled = settings.Enabled,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StartDate = settings.StartDate,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
Description = settings.Description,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
NotificationUrl = settings.NotificationUrl,
|
||||
Tagline = settings.Tagline,
|
||||
PerksTemplate = settings.PerksTemplate,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
UseInvoiceAmount = settings.UseInvoiceAmount,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery), settings.ResetEvery),
|
||||
UseAllStoreInvoices = settings.UseAllStoreInvoices,
|
||||
AppId = appId,
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
SortPerksByPopularity = settings.SortPerksByPopularity
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
[HttpPost]
|
||||
[Route("{appId}/settings/crowdfund")]
|
||||
public async Task<IActionResult> UpdateCrowdfund(string appId, UpdateCrowdfundViewModel vm)
|
||||
{
|
||||
if (!string.IsNullOrEmpty( vm.TargetCurrency) && _AppsHelper.GetCurrencyData(vm.TargetCurrency, false) == null)
|
||||
ModelState.AddModelError(nameof(vm.TargetCurrency), "Invalid currency");
|
||||
|
||||
try
|
||||
{
|
||||
_AppsHelper.Parse(vm.PerksTemplate, vm.TargetCurrency).ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PerksTemplate), "Invalid template");
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && !vm.StartDate.HasValue)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.StartDate), "A start date is needed when the goal resets every X amount of time.");
|
||||
}
|
||||
|
||||
if (Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery) != CrowdfundResetEvery.Never && vm.ResetEveryAmount <= 0)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ResetEveryAmount), "You must reset the goal at a minimum of 1 ");
|
||||
}
|
||||
|
||||
if (vm.DisplayPerksRanking && !vm.SortPerksByPopularity)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.DisplayPerksRanking), "You must sort by popularity in order to display ranking.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
var app = await GetOwnedApp(appId, AppType.Crowdfund);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var newSettings = new CrowdfundSettings()
|
||||
{
|
||||
Title = vm.Title,
|
||||
Enabled = vm.Enabled,
|
||||
EnforceTargetAmount = vm.EnforceTargetAmount,
|
||||
StartDate = vm.StartDate,
|
||||
TargetCurrency = vm.TargetCurrency,
|
||||
Description = _AppsHelper.Sanitize( vm.Description),
|
||||
EndDate = vm.EndDate,
|
||||
TargetAmount = vm.TargetAmount,
|
||||
CustomCSSLink = vm.CustomCSSLink,
|
||||
MainImageUrl = vm.MainImageUrl,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
NotificationUrl = vm.NotificationUrl,
|
||||
Tagline = vm.Tagline,
|
||||
PerksTemplate = vm.PerksTemplate,
|
||||
DisqusEnabled = vm.DisqusEnabled,
|
||||
SoundsEnabled = vm.SoundsEnabled,
|
||||
DisqusShortname = vm.DisqusShortname,
|
||||
AnimationsEnabled = vm.AnimationsEnabled,
|
||||
ResetEveryAmount = vm.ResetEveryAmount,
|
||||
ResetEvery = Enum.Parse<CrowdfundResetEvery>(vm.ResetEvery),
|
||||
UseInvoiceAmount = vm.UseInvoiceAmount,
|
||||
UseAllStoreInvoices = vm.UseAllStoreInvoices,
|
||||
DisplayPerksRanking = vm.DisplayPerksRanking,
|
||||
SortPerksByPopularity = vm.SortPerksByPopularity
|
||||
};
|
||||
|
||||
app.SetSettings(newSettings);
|
||||
await UpdateAppSettings(app);
|
||||
_EventAggregator.Publish(new CrowdfundAppUpdated()
|
||||
{
|
||||
AppId = appId,
|
||||
StoreId = app.StoreDataId,
|
||||
Settings = newSettings
|
||||
});
|
||||
StatusMessage = "App updated";
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new {appId});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
using System.Text;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
@ -65,6 +68,9 @@ namespace BTCPayServer.Controllers
|
||||
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
|
||||
public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?";
|
||||
public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
|
||||
public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 };
|
||||
public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF;
|
||||
|
||||
|
||||
public string CustomCSSLink { get; set; }
|
||||
}
|
||||
@ -87,6 +93,7 @@ namespace BTCPayServer.Controllers
|
||||
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
|
||||
CustomCSSLink = settings.CustomCSSLink
|
||||
};
|
||||
if (HttpContext?.Request != null)
|
||||
@ -157,6 +164,7 @@ namespace BTCPayServer.Controllers
|
||||
ButtonText = vm.ButtonText,
|
||||
CustomButtonText = vm.CustomButtonText,
|
||||
CustomTipText = vm.CustomTipText,
|
||||
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
|
||||
CustomCSSLink = vm.CustomCSSLink
|
||||
});
|
||||
await UpdateAppSettings(app);
|
||||
@ -174,5 +182,21 @@ namespace BTCPayServer.Controllers
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private int[] ListSplit(string list, string separator = ",")
|
||||
{
|
||||
if (string.IsNullOrEmpty(list))
|
||||
{
|
||||
return Array.Empty<int>();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove all characters except numeric and comma
|
||||
Regex charsToDestroy = new Regex(@"[^\d|\" + separator + "]");
|
||||
list = charsToDestroy.Replace(list, "");
|
||||
|
||||
return list.Split(separator, System.StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,17 +24,20 @@ namespace BTCPayServer.Controllers
|
||||
public AppsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
EventAggregator eventAggregator,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
AppsHelper appsHelper)
|
||||
{
|
||||
_UserManager = userManager;
|
||||
_ContextFactory = contextFactory;
|
||||
_EventAggregator = eventAggregator;
|
||||
_NetworkProvider = networkProvider;
|
||||
_AppsHelper = appsHelper;
|
||||
}
|
||||
|
||||
private UserManager<ApplicationUser> _UserManager;
|
||||
private ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private BTCPayNetworkProvider _NetworkProvider;
|
||||
private AppsHelper _AppsHelper;
|
||||
|
||||
@ -44,7 +47,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public async Task<IActionResult> ListApps()
|
||||
{
|
||||
var apps = await GetAllApps();
|
||||
var apps = await _AppsHelper.GetAllApps(GetUserId());
|
||||
return View(new ListAppsViewModel()
|
||||
{
|
||||
Apps = apps
|
||||
@ -58,7 +61,7 @@ namespace BTCPayServer.Controllers
|
||||
var appData = await GetOwnedApp(appId);
|
||||
if (appData == null)
|
||||
return NotFound();
|
||||
if (await DeleteApp(appData))
|
||||
if (await _AppsHelper.DeleteApp(appData))
|
||||
StatusMessage = "App removed successfully";
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
@ -67,7 +70,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp()
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must have created at least one store";
|
||||
@ -82,7 +85,7 @@ namespace BTCPayServer.Controllers
|
||||
[Route("create")]
|
||||
public async Task<IActionResult> CreateApp(CreateAppViewModel vm)
|
||||
{
|
||||
var stores = await GetOwnedStores();
|
||||
var stores = await _AppsHelper.GetOwnedStores(GetUserId());
|
||||
if (stores.Length == 0)
|
||||
{
|
||||
StatusMessage = "Error: You must own at least one store";
|
||||
@ -117,9 +120,17 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
StatusMessage = "App successfully created";
|
||||
CreatedAppId = id;
|
||||
if (appType == AppType.PointOfSale)
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
|
||||
switch (appType)
|
||||
{
|
||||
case AppType.PointOfSale:
|
||||
return RedirectToAction(nameof(UpdatePointOfSale), new { appId = id });
|
||||
case AppType.Crowdfund:
|
||||
return RedirectToAction(nameof(UpdateCrowdfund), new { appId = id });
|
||||
default:
|
||||
return RedirectToAction(nameof(ListApps));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -142,50 +153,7 @@ namespace BTCPayServer.Controllers
|
||||
return _AppsHelper.GetAppDataIfOwner(GetUserId(), appId, type);
|
||||
}
|
||||
|
||||
private async Task<StoreData[]> GetOwnedStores()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId && us.Role == StoreRoles.Owner)
|
||||
.Select(u => u.StoreData)
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> DeleteApp(AppData appData)
|
||||
{
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
ctx.Apps.Add(appData);
|
||||
ctx.Entry<AppData>(appData).State = EntityState.Deleted;
|
||||
return await ctx.SaveChangesAsync() == 1;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ListAppsViewModel.ListAppViewModel[]> GetAllApps()
|
||||
{
|
||||
var userId = GetUserId();
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
return await ctx.UserStore
|
||||
.Where(us => us.ApplicationUserId == userId)
|
||||
.Join(ctx.Apps, us => us.StoreDataId, app => app.StoreDataId,
|
||||
(us, app) =>
|
||||
new ListAppsViewModel.ListAppViewModel()
|
||||
{
|
||||
IsOwner = us.Role == StoreRoles.Owner,
|
||||
StoreId = us.StoreDataId,
|
||||
StoreName = us.StoreData.StoreName,
|
||||
AppName = app.Name,
|
||||
AppType = app.AppType,
|
||||
Id = app.Id
|
||||
})
|
||||
.ToArrayAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private string GetUserId()
|
||||
{
|
||||
return _UserManager.GetUserId(User);
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -7,14 +8,21 @@ using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Hubs;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitpayClient;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using static BTCPayServer.Controllers.AppsController;
|
||||
|
||||
@ -22,14 +30,20 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public class AppsPublicController : Controller
|
||||
{
|
||||
public AppsPublicController(AppsHelper appsHelper, InvoiceController invoiceController)
|
||||
public AppsPublicController(AppsHelper appsHelper,
|
||||
InvoiceController invoiceController,
|
||||
CrowdfundHubStreamer crowdfundHubStreamer, UserManager<ApplicationUser> userManager)
|
||||
{
|
||||
_AppsHelper = appsHelper;
|
||||
_InvoiceController = invoiceController;
|
||||
_CrowdfundHubStreamer = crowdfundHubStreamer;
|
||||
_UserManager = userManager;
|
||||
}
|
||||
|
||||
private AppsHelper _AppsHelper;
|
||||
private InvoiceController _InvoiceController;
|
||||
private readonly CrowdfundHubStreamer _CrowdfundHubStreamer;
|
||||
private readonly UserManager<ApplicationUser> _UserManager;
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
@ -65,9 +79,125 @@ namespace BTCPayServer.Controllers
|
||||
ButtonText = settings.ButtonText,
|
||||
CustomButtonText = settings.CustomButtonText,
|
||||
CustomTipText = settings.CustomTipText,
|
||||
CustomCSSLink = settings.CustomCSSLink
|
||||
CustomTipPercentages = settings.CustomTipPercentages,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
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")]
|
||||
@ -125,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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,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()
|
||||
{
|
||||
@ -209,6 +442,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
public string GetDetailString(string field)
|
||||
{
|
||||
|
||||
return GetDetail(field).FirstOrDefault()?.Value?.Value;
|
||||
}
|
||||
}
|
||||
@ -244,5 +478,6 @@ namespace BTCPayServer.Controllers
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -66,15 +66,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
if (dateEnd != null)
|
||||
dateEnd = dateEnd.Value + TimeSpan.FromDays(1); //Should include the end day
|
||||
|
||||
|
||||
var query = new InvoiceQuery()
|
||||
{
|
||||
Count = limit,
|
||||
Skip = offset,
|
||||
EndDate = dateEnd,
|
||||
StartDate = dateStart,
|
||||
OrderId = orderId,
|
||||
ItemCode = itemCode,
|
||||
OrderId = orderId == null ? null : new[] { orderId },
|
||||
ItemCode = itemCode == null ? null : new[] { itemCode },
|
||||
Status = status == null ? null : new[] { status },
|
||||
StoreId = new[] { this.HttpContext.GetStoreData().Id }
|
||||
};
|
||||
|
@ -13,6 +13,7 @@ using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -258,6 +259,11 @@ namespace BTCPayServer.Controllers
|
||||
storeBlob.ChangellySettings.IsConfigured())
|
||||
? storeBlob.ChangellySettings
|
||||
: null;
|
||||
|
||||
CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled &&
|
||||
storeBlob.CoinSwitchSettings.IsConfigured())
|
||||
? storeBlob.CoinSwitchSettings
|
||||
: null;
|
||||
|
||||
|
||||
var changellyAmountDue = changelly != null
|
||||
@ -304,11 +310,14 @@ namespace BTCPayServer.Controllers
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
Status = invoice.StatusString,
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
NetworkFee = paymentMethodDetails.GetTxFee(),
|
||||
NetworkFee = paymentMethodDetails.GetNextNetworkFee(),
|
||||
IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1,
|
||||
ChangellyEnabled = changelly != null,
|
||||
ChangellyMerchantId = changelly?.ChangellyMerchantId,
|
||||
ChangellyAmountDue = changellyAmountDue,
|
||||
CoinSwitchEnabled = coinswitch != null,
|
||||
CoinSwitchMerchantId = coinswitch?.MerchantId,
|
||||
CoinSwitchMode = coinswitch?.Mode,
|
||||
StoreId = store.Id,
|
||||
AvailableCryptos = invoice.GetPaymentMethods(_NetworkProvider)
|
||||
.Where(i => i.Network != null)
|
||||
@ -473,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;
|
||||
@ -484,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);
|
||||
@ -638,13 +649,13 @@ namespace BTCPayServer.Controllers
|
||||
if (newState == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, InvoiceEvent.MarkedInvalid));
|
||||
StatusMessage = "Invoice marked invalid";
|
||||
}
|
||||
else if(newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, "invoice_markedComplete"));
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, InvoiceEvent.MarkedCompleted));
|
||||
StatusMessage = "Invoice marked complete";
|
||||
}
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Payments;
|
||||
@ -159,7 +160,7 @@ namespace BTCPayServer.Controllers
|
||||
entity.PosData = invoice.PosData;
|
||||
entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity, logs, _NetworkProvider);
|
||||
await fetchingAll;
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, "invoice_created"));
|
||||
_EventAggregator.Publish(new Events.InvoiceEvent(entity.EntityToDTO(_NetworkProvider), 1001, InvoiceEvent.Created));
|
||||
var resp = entity.EntityToDTO(_NetworkProvider);
|
||||
return new DataWrapper<InvoiceResponse>(resp) { Facade = "pos/invoice" };
|
||||
}
|
||||
@ -200,8 +201,6 @@ namespace BTCPayServer.Controllers
|
||||
paymentMethod.SetId(supportedPaymentMethod.PaymentId);
|
||||
paymentMethod.Rate = rate.BidAsk.Bid;
|
||||
var paymentDetails = await handler.CreatePaymentMethodDetails(supportedPaymentMethod, paymentMethod, store, network, preparePayment);
|
||||
if (storeBlob.NetworkFeeDisabled)
|
||||
paymentDetails.SetNoTxFee();
|
||||
paymentMethod.SetPaymentMethodDetails(paymentDetails);
|
||||
|
||||
Func<Money, Money, bool> compare = null;
|
||||
@ -241,7 +240,7 @@ namespace BTCPayServer.Controllers
|
||||
#pragma warning disable CS0618
|
||||
if (paymentMethod.GetId().IsBTCOnChain)
|
||||
{
|
||||
entity.TxFee = paymentMethod.TxFee;
|
||||
entity.TxFee = paymentMethod.NextNetworkFee;
|
||||
entity.Rate = paymentMethod.Rate;
|
||||
entity.DepositAddress = paymentMethod.DepositAddress;
|
||||
}
|
||||
|
@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
|
||||
[Route("embed/{storeId}/{cryptoCode}/ln")]
|
||||
[AllowAnonymous]
|
||||
public class PublicLightningNodeInfoController : Controller
|
||||
{
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly LightningLikePaymentHandler _LightningLikePaymentHandler;
|
||||
private readonly StoreRepository _StoreRepository;
|
||||
|
||||
public PublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
LightningLikePaymentHandler lightningLikePaymentHandler, StoreRepository storeRepository)
|
||||
{
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_LightningLikePaymentHandler = lightningLikePaymentHandler;
|
||||
_StoreRepository = storeRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ShowLightningNodeInfo(string storeId, string cryptoCode)
|
||||
{
|
||||
var store = await _StoreRepository.FindStore(storeId);
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
var paymentMethodDetails = GetExistingLightningSupportedPaymentMethod(cryptoCode, store);
|
||||
var network = _BtcPayNetworkProvider.GetNetwork(cryptoCode);
|
||||
var nodeInfo =
|
||||
await _LightningLikePaymentHandler.GetNodeInfo(paymentMethodDetails,
|
||||
network);
|
||||
|
||||
return View(new ShowLightningNodeInfoViewModel()
|
||||
{
|
||||
Available = true,
|
||||
NodeInfo = nodeInfo.ToString(),
|
||||
CryptoCode = cryptoCode,
|
||||
CryptoImage = GetImage(paymentMethodDetails.PaymentId, network)
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return View(new ShowLightningNodeInfoViewModel() {Available = false, CryptoCode = cryptoCode});
|
||||
}
|
||||
}
|
||||
|
||||
private LightningSupportedPaymentMethod GetExistingLightningSupportedPaymentMethod(string cryptoCode, StoreData store)
|
||||
{
|
||||
var id = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
|
||||
var existing = store.GetSupportedPaymentMethods(_BtcPayNetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
.FirstOrDefault(d => d.PaymentId == id);
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
||||
? Url.Content(network.CryptoImagePath)
|
||||
: Url.Content(network.LightningImagePath);
|
||||
return "/" + res;
|
||||
}
|
||||
}
|
||||
|
||||
public class ShowLightningNodeInfoViewModel
|
||||
{
|
||||
public string NodeInfo { get; set; }
|
||||
public bool Available { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string CryptoImage { get; set; }
|
||||
}
|
||||
}
|
@ -46,7 +46,7 @@ namespace BTCPayServer.Controllers
|
||||
string storeId,
|
||||
string cryptoCode,
|
||||
string command,
|
||||
int account = 0)
|
||||
string keyPath = "")
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
return NotFound();
|
||||
@ -67,7 +67,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (command == "getxpub")
|
||||
{
|
||||
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
|
||||
var k = KeyPath.Parse(keyPath);
|
||||
if (k.Indexes.Length == 0)
|
||||
throw new FormatException("Invalid key path");
|
||||
var getxpubResult = await hw.GetExtPubKey(network, k, normalOperationTimeout.Token);
|
||||
result = getxpubResult;
|
||||
}
|
||||
}
|
||||
@ -171,7 +174,8 @@ namespace BTCPayServer.Controllers
|
||||
if (strategy != null)
|
||||
await wallet.TrackAsync(strategy.DerivationStrategyBase);
|
||||
store.SetSupportedPaymentMethod(paymentMethodId, strategy);
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
storeBlob.SetExcluded(paymentMethodId, willBeExcluded);
|
||||
storeBlob.SetWalletKeyPathRoot(paymentMethodId, vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath));
|
||||
store.SetStoreBlob(storeBlob);
|
||||
}
|
||||
catch
|
||||
|
72
BTCPayServer/Controllers/StoresController.CoinSwitch.cs
Normal file
72
BTCPayServer/Controllers/StoresController.CoinSwitch.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class StoresController
|
||||
{
|
||||
[HttpGet]
|
||||
[Route("{storeId}/coinswitch")]
|
||||
public IActionResult UpdateCoinSwitchSettings(string storeId)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
UpdateCoinSwitchSettingsViewModel vm = new UpdateCoinSwitchSettingsViewModel();
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private void SetExistingValues(StoreData store, UpdateCoinSwitchSettingsViewModel vm)
|
||||
{
|
||||
|
||||
var existing = store.GetStoreBlob().CoinSwitchSettings;
|
||||
if (existing == null) return;
|
||||
vm.MerchantId = existing.MerchantId;
|
||||
vm.Enabled = existing.Enabled;
|
||||
vm.Mode = existing.Mode;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Route("{storeId}/coinswitch")]
|
||||
public async Task<IActionResult> UpdateCoinSwitchSettings(string storeId, UpdateCoinSwitchSettingsViewModel vm,
|
||||
string command)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null)
|
||||
return NotFound();
|
||||
if (vm.Enabled)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
var coinSwitchSettings = new CoinSwitchSettings()
|
||||
{
|
||||
MerchantId = vm.MerchantId,
|
||||
Enabled = vm.Enabled,
|
||||
Mode = vm.Mode
|
||||
};
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "save":
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
storeBlob.CoinSwitchSettings = coinSwitchSettings;
|
||||
store.SetStoreBlob(storeBlob);
|
||||
await _Repo.UpdateStore(store);
|
||||
StatusMessage = "CoinSwitch settings modified";
|
||||
return RedirectToAction(nameof(UpdateStore), new {
|
||||
storeId});
|
||||
|
||||
default:
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -27,7 +27,8 @@ namespace BTCPayServer.Controllers
|
||||
LightningNodeViewModel vm = new LightningNodeViewModel
|
||||
{
|
||||
CryptoCode = cryptoCode,
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString()
|
||||
InternalLightningNode = GetInternalLighningNode(cryptoCode)?.ToString(),
|
||||
StoreId = storeId
|
||||
};
|
||||
SetExistingValues(store, vm);
|
||||
return View(vm);
|
||||
@ -154,7 +155,7 @@ namespace BTCPayServer.Controllers
|
||||
var handler = (LightningLikePaymentHandler)_ServiceProvider.GetRequiredService<IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>>();
|
||||
try
|
||||
{
|
||||
var info = await handler.Test(paymentMethod, network);
|
||||
var info = await handler.GetNodeInfo(paymentMethod, network);
|
||||
if (!vm.SkipPortTest)
|
||||
{
|
||||
using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
|
||||
|
@ -10,7 +10,9 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Services;
|
||||
@ -406,7 +408,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.Id = store.Id;
|
||||
vm.StoreName = store.StoreName;
|
||||
vm.StoreWebsite = store.StoreWebsite;
|
||||
vm.NetworkFee = !storeBlob.NetworkFeeDisabled;
|
||||
vm.NetworkFeeMode = storeBlob.NetworkFeeMode;
|
||||
vm.AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice;
|
||||
vm.SpeedPolicy = store.SpeedPolicy;
|
||||
vm.CanDelete = _Repo.CanDeleteStores();
|
||||
@ -464,6 +466,14 @@ namespace BTCPayServer.Controllers
|
||||
Action = nameof(UpdateChangellySettings),
|
||||
Provider = "Changelly"
|
||||
});
|
||||
|
||||
var coinSwitchEnabled = storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled;
|
||||
vm.ThirdPartyPaymentMethods.Add(new StoreViewModel.ThirdPartyPaymentMethod()
|
||||
{
|
||||
Enabled = coinSwitchEnabled,
|
||||
Action = nameof(UpdateCoinSwitchSettings),
|
||||
Provider = "CoinSwitch"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -489,7 +499,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var blob = StoreData.GetStoreBlob();
|
||||
blob.AnyoneCanInvoice = model.AnyoneCanCreateInvoice;
|
||||
blob.NetworkFeeDisabled = !model.NetworkFee;
|
||||
blob.NetworkFeeMode = model.NetworkFeeMode;
|
||||
blob.MonitoringExpiration = model.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = model.InvoiceExpiration;
|
||||
blob.LightningDescriptionTemplate = model.LightningDescriptionTemplate ?? string.Empty;
|
||||
|
@ -410,8 +410,8 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
var cryptoCode = walletId.CryptoCode;
|
||||
var storeBlob = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
var derivationScheme = GetPaymentMethod(walletId, storeBlob).DerivationStrategyBase;
|
||||
var storeData = (await Repository.FindStore(walletId.StoreId, GetUserId()));
|
||||
var derivationScheme = GetPaymentMethod(walletId, storeData).DerivationStrategyBase;
|
||||
|
||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
|
||||
@ -476,15 +476,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||
}
|
||||
if (command == "getinfo")
|
||||
{
|
||||
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
|
||||
{
|
||||
throw new Exception($"This store is not configured to use this ledger");
|
||||
}
|
||||
result = new GetInfoResult();
|
||||
}
|
||||
if (command == "test")
|
||||
{
|
||||
result = await hw.Test(normalOperationTimeout.Token);
|
||||
@ -514,10 +505,20 @@ namespace BTCPayServer.Controllers
|
||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||
}
|
||||
|
||||
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||
if (foundKeyPath == null)
|
||||
var storeBlob = storeData.GetStoreBlob();
|
||||
var paymentId = new Payments.PaymentMethodId(cryptoCode, Payments.PaymentTypes.BTCLike);
|
||||
var foundKeyPath = storeBlob.GetWalletKeyPathRoot(paymentId);
|
||||
// Some deployment have the wallet root key path saved in the store blob
|
||||
// If it does, we only have to make 1 call to the hw to check if it can sign the given strategy,
|
||||
if (foundKeyPath == null || !await hw.CanSign(network, strategy, foundKeyPath, normalOperationTimeout.Token))
|
||||
{
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
|
||||
foundKeyPath = await hw.FindKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||
if (foundKeyPath == null)
|
||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||
storeBlob.SetWalletKeyPathRoot(paymentId, foundKeyPath);
|
||||
storeData.SetStoreBlob(storeBlob);
|
||||
await Repository.UpdateStore(storeData);
|
||||
}
|
||||
|
||||
TransactionBuilder builder = network.NBitcoinNetwork.CreateTransactionBuilder();
|
||||
|
55
BTCPayServer/Crowdfund/CrowdfundHub.cs
Normal file
55
BTCPayServer/Crowdfund/CrowdfundHub.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace BTCPayServer.Hubs
|
||||
{
|
||||
public class CrowdfundHub: Hub
|
||||
{
|
||||
public const string InvoiceCreated = "InvoiceCreated";
|
||||
public const string PaymentReceived = "PaymentReceived";
|
||||
public const string InfoUpdated = "InfoUpdated";
|
||||
public const string InvoiceError = "InvoiceError";
|
||||
private readonly AppsPublicController _AppsPublicController;
|
||||
|
||||
public CrowdfundHub(AppsPublicController appsPublicController)
|
||||
{
|
||||
_AppsPublicController = appsPublicController;
|
||||
}
|
||||
public async Task ListenToCrowdfundApp(string appId)
|
||||
{
|
||||
if (Context.Items.ContainsKey("app"))
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.Items["app"].ToString());
|
||||
Context.Items.Remove("app");
|
||||
}
|
||||
Context.Items.Add("app", appId);
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, appId);
|
||||
}
|
||||
|
||||
|
||||
public async Task CreateInvoice(ContributeToCrowdfund model)
|
||||
{
|
||||
model.RedirectToCheckout = false;
|
||||
_AppsPublicController.ControllerContext.HttpContext = Context.GetHttpContext();
|
||||
var result = await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
|
||||
switch (result)
|
||||
{
|
||||
case OkObjectResult okObjectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceCreated, new[] {okObjectResult.Value.ToString()});
|
||||
break;
|
||||
case ObjectResult objectResult:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, new[] {objectResult.Value});
|
||||
break;
|
||||
default:
|
||||
await Clients.Caller.SendCoreAsync(InvoiceError, System.Array.Empty<object>());
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
365
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal file
365
BTCPayServer/Crowdfund/CrowdfundHubStreamer.cs
Normal file
@ -0,0 +1,365 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NBitcoin;
|
||||
using YamlDotNet.Core;
|
||||
|
||||
namespace BTCPayServer.Hubs
|
||||
{
|
||||
public class
|
||||
CrowdfundHubStreamer
|
||||
{
|
||||
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app_";
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly IHubContext<CrowdfundHub> _HubContext;
|
||||
private readonly IMemoryCache _MemoryCache;
|
||||
private readonly AppsHelper _AppsHelper;
|
||||
private readonly RateFetcher _RateFetcher;
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
private readonly ILogger<CrowdfundHubStreamer> _Logger;
|
||||
|
||||
private readonly ConcurrentDictionary<string,(string appId, bool useAllStoreInvoices,bool useInvoiceAmount)> _QuickAppInvoiceLookup =
|
||||
new ConcurrentDictionary<string, (string appId, bool useAllStoreInvoices, bool useInvoiceAmount)>();
|
||||
|
||||
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
||||
IHubContext<CrowdfundHub> hubContext,
|
||||
IMemoryCache memoryCache,
|
||||
AppsHelper appsHelper,
|
||||
RateFetcher rateFetcher,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
ILogger<CrowdfundHubStreamer> logger)
|
||||
{
|
||||
_EventAggregator = eventAggregator;
|
||||
_HubContext = hubContext;
|
||||
_MemoryCache = memoryCache;
|
||||
_AppsHelper = appsHelper;
|
||||
_RateFetcher = rateFetcher;
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
_Logger = logger;
|
||||
#pragma warning disable 4014
|
||||
InitLookup();
|
||||
#pragma warning restore 4014
|
||||
SubscribeToEvents();
|
||||
}
|
||||
|
||||
private async Task InitLookup()
|
||||
{
|
||||
var apps = await _AppsHelper.GetAllApps(null, true);
|
||||
apps = apps.Where(model => Enum.Parse<AppType>(model.AppType) == AppType.Crowdfund).ToArray();
|
||||
var tasks = new List<Task>();
|
||||
tasks.AddRange(apps.Select(app => Task.Run(async () =>
|
||||
{
|
||||
var fullApp = await _AppsHelper.GetApp(app.Id, AppType.Crowdfund, false);
|
||||
var settings = fullApp.GetSettings<AppsController.CrowdfundSettings>();
|
||||
UpdateLookup(app.Id, app.StoreId, settings);
|
||||
})));
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private void UpdateLookup(string appId, string storeId, AppsController.CrowdfundSettings settings)
|
||||
{
|
||||
_QuickAppInvoiceLookup.AddOrReplace(storeId,
|
||||
(
|
||||
appId: appId,
|
||||
useAllStoreInvoices: settings?.UseAllStoreInvoices ?? false,
|
||||
useInvoiceAmount: settings?.UseInvoiceAmount ?? false
|
||||
));
|
||||
}
|
||||
|
||||
public Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
||||
{
|
||||
return _MemoryCache.GetOrCreateAsync(GetCacheKey(appId), async entry =>
|
||||
{
|
||||
_Logger.LogInformation($"GetCrowdfundInfo {appId}");
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
var result = await GetInfo(app);
|
||||
entry.SetValue(result);
|
||||
|
||||
TimeSpan? expire = null;
|
||||
|
||||
if (result.StartDate.HasValue && result.StartDate < DateTime.Now)
|
||||
{
|
||||
expire = result.StartDate.Value.Subtract(DateTime.Now);
|
||||
}
|
||||
else if (result.EndDate.HasValue && result.EndDate > DateTime.Now)
|
||||
{
|
||||
expire = result.EndDate.Value.Subtract(DateTime.Now);
|
||||
}
|
||||
if(!expire.HasValue || expire?.TotalMinutes > 5 || expire?.TotalMilliseconds <= 0)
|
||||
{
|
||||
expire = TimeSpan.FromMinutes(5);
|
||||
}
|
||||
|
||||
entry.AbsoluteExpirationRelativeToNow = expire;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
|
||||
_EventAggregator.Subscribe<InvoiceEvent>(OnInvoiceEvent);
|
||||
_EventAggregator.Subscribe<AppsController.CrowdfundAppUpdated>(updated =>
|
||||
{
|
||||
UpdateLookup(updated.AppId, updated.StoreId, updated.Settings);
|
||||
InvalidateCacheForApp(updated.AppId);
|
||||
});
|
||||
}
|
||||
|
||||
private string GetCacheKey(string appId)
|
||||
{
|
||||
return $"{CrowdfundInvoiceOrderIdPrefix}:{appId}";
|
||||
}
|
||||
|
||||
private void OnInvoiceEvent(InvoiceEvent invoiceEvent)
|
||||
{
|
||||
if (!_QuickAppInvoiceLookup.TryGetValue(invoiceEvent.Invoice.StoreId, out var quickLookup) ||
|
||||
(!quickLookup.useAllStoreInvoices &&
|
||||
!string.IsNullOrEmpty(invoiceEvent.Invoice.OrderId) &&
|
||||
!invoiceEvent.Invoice.OrderId.Equals($"{CrowdfundInvoiceOrderIdPrefix}{quickLookup.appId}", StringComparison.InvariantCulture)
|
||||
))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (invoiceEvent.Name)
|
||||
{
|
||||
case InvoiceEvent.ReceivedPayment:
|
||||
var data = invoiceEvent.Payment.GetCryptoPaymentData();
|
||||
_HubContext.Clients.Group(quickLookup.appId).SendCoreAsync(CrowdfundHub.PaymentReceived, new object[]
|
||||
{
|
||||
data.GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
Enum.GetName(typeof(PaymentTypes),
|
||||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||
} );
|
||||
|
||||
InvalidateCacheForApp(quickLookup.appId);
|
||||
break;
|
||||
case InvoiceEvent.Created:
|
||||
case InvoiceEvent.MarkedInvalid:
|
||||
case InvoiceEvent.MarkedCompleted:
|
||||
if (quickLookup.useInvoiceAmount)
|
||||
{
|
||||
InvalidateCacheForApp(quickLookup.appId);
|
||||
}
|
||||
break;
|
||||
case InvoiceEvent.Completed:
|
||||
InvalidateCacheForApp(quickLookup.appId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateCacheForApp(string appId)
|
||||
{
|
||||
_Logger.LogInformation($"App {appId} cache invalidated");
|
||||
_MemoryCache.Remove(GetCacheKey(appId));
|
||||
|
||||
GetCrowdfundInfo(appId).ContinueWith(task =>
|
||||
{
|
||||
_HubContext.Clients.Group(appId).SendCoreAsync(CrowdfundHub.InfoUpdated, new object[]{ task.Result} );
|
||||
}, TaskScheduler.Current);
|
||||
|
||||
}
|
||||
|
||||
private async Task<decimal> GetCurrentContributionAmount(Dictionary<string, decimal> stats, string primaryCurrency, RateRules rateRules)
|
||||
{
|
||||
decimal result = 0;
|
||||
|
||||
var ratesTask = _RateFetcher .FetchRates(
|
||||
stats.Keys
|
||||
.Select((x) => new CurrencyPair( primaryCurrency, PaymentMethodId.Parse(x).CryptoCode))
|
||||
.Distinct()
|
||||
.ToHashSet(),
|
||||
rateRules);
|
||||
|
||||
var finalTasks = new List<Task>();
|
||||
foreach (var rateTask in ratesTask)
|
||||
{
|
||||
finalTasks.Add(Task.Run(async () =>
|
||||
{
|
||||
var tResult = await rateTask.Value;
|
||||
var rate = tResult.BidAsk?.Bid;
|
||||
if (rate == null) return;
|
||||
|
||||
foreach (var stat in stats)
|
||||
{
|
||||
if (string.Equals(PaymentMethodId.Parse(stat.Key).CryptoCode, rateTask.Key.Right,
|
||||
StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
result += (1m / rate.Value) * stat.Value;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(finalTasks);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Dictionary<string, decimal> GetCurrentContributionAmountStats(InvoiceEntity[] invoices, bool usePaymentData = true)
|
||||
{
|
||||
if(usePaymentData){
|
||||
var payments = invoices.SelectMany(entity => entity.GetPayments());
|
||||
|
||||
var groupedByMethod = payments.GroupBy(entity => entity.GetPaymentMethodId());
|
||||
|
||||
return groupedByMethod.ToDictionary(entities => entities.Key.ToString(),
|
||||
entities => entities.Sum(entity => entity.GetCryptoPaymentData().GetValue()));
|
||||
}
|
||||
else
|
||||
{
|
||||
return invoices
|
||||
.GroupBy(entity => entity.ProductInformation.Currency)
|
||||
.ToDictionary(
|
||||
entities => entities.Key,
|
||||
entities => entities.Sum(entity => entity.ProductInformation.Price));
|
||||
}
|
||||
}
|
||||
private async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, string statusMessage= null)
|
||||
{
|
||||
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
|
||||
|
||||
var resetEvery = settings.StartDate.HasValue? settings.ResetEvery : CrowdfundResetEvery.Never;
|
||||
DateTime? lastResetDate = null;
|
||||
DateTime? nextResetDate = null;
|
||||
if (resetEvery != CrowdfundResetEvery.Never)
|
||||
{
|
||||
lastResetDate = settings.StartDate.Value;
|
||||
|
||||
nextResetDate = lastResetDate.Value;
|
||||
while (DateTime.Now >= nextResetDate)
|
||||
{
|
||||
lastResetDate = nextResetDate;
|
||||
switch (resetEvery)
|
||||
{
|
||||
case CrowdfundResetEvery.Hour:
|
||||
nextResetDate = lastResetDate.Value.AddHours(settings.ResetEveryAmount);
|
||||
break;
|
||||
case CrowdfundResetEvery.Day:
|
||||
nextResetDate = lastResetDate.Value.AddDays(settings.ResetEveryAmount);
|
||||
break;
|
||||
case CrowdfundResetEvery.Month:
|
||||
|
||||
nextResetDate = lastResetDate.Value.AddMonths(settings.ResetEveryAmount);
|
||||
break;
|
||||
case CrowdfundResetEvery.Year:
|
||||
nextResetDate = lastResetDate.Value.AddYears(settings.ResetEveryAmount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invoices = await GetInvoicesForApp(settings.UseAllStoreInvoices? null : appData.Id, lastResetDate);
|
||||
var completeInvoices = invoices.Where(entity => entity.Status == InvoiceStatus.Complete).ToArray();
|
||||
var pendingInvoices = invoices.Where(entity => entity.Status != InvoiceStatus.Complete).ToArray();
|
||||
|
||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
|
||||
|
||||
var pendingPaymentStats = GetCurrentContributionAmountStats(pendingInvoices, !settings.UseInvoiceAmount);
|
||||
var paymentStats = GetCurrentContributionAmountStats(completeInvoices, !settings.UseInvoiceAmount);
|
||||
|
||||
var currentAmount = await GetCurrentContributionAmount(
|
||||
paymentStats,
|
||||
settings.TargetCurrency, rateRules);
|
||||
var currentPendingAmount = await GetCurrentContributionAmount(
|
||||
pendingPaymentStats,
|
||||
settings.TargetCurrency, rateRules);
|
||||
|
||||
|
||||
|
||||
|
||||
var perkCount = invoices
|
||||
.Where(entity => !string.IsNullOrEmpty( entity.ProductInformation.ItemCode))
|
||||
.GroupBy(entity => entity.ProductInformation.ItemCode)
|
||||
.ToDictionary(entities => entities.Key, entities => entities.Count());
|
||||
|
||||
var perks = _AppsHelper.Parse(settings.PerksTemplate, settings.TargetCurrency);
|
||||
if (settings.SortPerksByPopularity)
|
||||
{
|
||||
var ordered = perkCount.OrderByDescending(pair => pair.Value);
|
||||
var newPerksOrder = ordered
|
||||
.Select(keyValuePair => perks.SingleOrDefault(item => item.Id == keyValuePair.Key))
|
||||
.Where(matchingPerk => matchingPerk != null)
|
||||
.ToList();
|
||||
var remainingPerks = perks.Where(item => !newPerksOrder.Contains(item));
|
||||
newPerksOrder.AddRange(remainingPerks);
|
||||
perks = newPerksOrder.ToArray();
|
||||
}
|
||||
return new ViewCrowdfundViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Tagline = settings.Tagline,
|
||||
Description = settings.Description,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
StoreId = appData.StoreDataId,
|
||||
AppId = appData.Id,
|
||||
StartDate = settings.StartDate?.ToUniversalTime(),
|
||||
EndDate = settings.EndDate?.ToUniversalTime(),
|
||||
TargetAmount = settings.TargetAmount,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StatusMessage = statusMessage,
|
||||
Perks = perks,
|
||||
DisqusEnabled = settings.DisqusEnabled,
|
||||
SoundsEnabled = settings.SoundsEnabled,
|
||||
DisqusShortname = settings.DisqusShortname,
|
||||
AnimationsEnabled = settings.AnimationsEnabled,
|
||||
ResetEveryAmount = settings.ResetEveryAmount,
|
||||
DisplayPerksRanking = settings.DisplayPerksRanking,
|
||||
PerkCount = perkCount,
|
||||
ResetEvery = Enum.GetName(typeof(CrowdfundResetEvery),settings.ResetEvery),
|
||||
CurrencyData = _AppsHelper.GetCurrencyData(settings.TargetCurrency, true),
|
||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||
{
|
||||
TotalContributors = invoices.Length,
|
||||
CurrentPendingAmount = currentPendingAmount,
|
||||
CurrentAmount = currentAmount,
|
||||
ProgressPercentage = (currentAmount/ settings.TargetAmount) * 100,
|
||||
PendingProgressPercentage = ( currentPendingAmount/ settings.TargetAmount) * 100,
|
||||
LastUpdated = DateTime.Now,
|
||||
PaymentStats = paymentStats,
|
||||
PendingPaymentStats = pendingPaymentStats,
|
||||
LastResetDate = lastResetDate,
|
||||
NextResetDate = nextResetDate
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<InvoiceEntity[]> GetInvoicesForApp(string appId, DateTime? startDate = null)
|
||||
{
|
||||
return await _InvoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
OrderId = appId == null? null : new []{$"{CrowdfundInvoiceOrderIdPrefix}{appId}"},
|
||||
Status = new string[]{
|
||||
InvoiceState.ToString(InvoiceStatus.New),
|
||||
InvoiceState.ToString(InvoiceStatus.Paid),
|
||||
InvoiceState.ToString(InvoiceStatus.Confirmed),
|
||||
InvoiceState.ToString(InvoiceStatus.Complete)},
|
||||
StartDate = startDate
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Services;
|
||||
using System.Security.Claims;
|
||||
using BTCPayServer.Payments.Changelly;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Rating;
|
||||
|
||||
@ -249,6 +250,12 @@ namespace BTCPayServer.Data
|
||||
}
|
||||
}
|
||||
|
||||
public enum NetworkFeeMode
|
||||
{
|
||||
MultiplePaymentsOnly,
|
||||
Always,
|
||||
Never
|
||||
}
|
||||
public class StoreBlob
|
||||
{
|
||||
public StoreBlob()
|
||||
@ -258,10 +265,21 @@ namespace BTCPayServer.Data
|
||||
PaymentTolerance = 0;
|
||||
RequiresRefundEmail = true;
|
||||
}
|
||||
public bool NetworkFeeDisabled
|
||||
|
||||
[Obsolete("Use NetworkFeeMode instead")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public bool? NetworkFeeDisabled
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
|
||||
public NetworkFeeMode NetworkFeeMode
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public bool RequiresRefundEmail { get; set; }
|
||||
|
||||
public string DefaultLang { get; set; }
|
||||
@ -305,6 +323,7 @@ namespace BTCPayServer.Data
|
||||
public bool AnyoneCanInvoice { get; set; }
|
||||
|
||||
public ChangellySettings ChangellySettings { get; set; }
|
||||
public CoinSwitchSettings CoinSwitchSettings { get; set; }
|
||||
|
||||
|
||||
string _LightningDescriptionTemplate;
|
||||
@ -366,6 +385,23 @@ namespace BTCPayServer.Data
|
||||
|
||||
[Obsolete("Use GetExcludedPaymentMethods instead")]
|
||||
public string[] ExcludedPaymentMethods { get; set; }
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public void SetWalletKeyPathRoot(PaymentMethodId paymentMethodId, KeyPath keyPath)
|
||||
{
|
||||
if (keyPath == null)
|
||||
WalletKeyPathRoots.Remove(paymentMethodId.ToString());
|
||||
else
|
||||
WalletKeyPathRoots.AddOrReplace(paymentMethodId.ToString().ToLowerInvariant(), keyPath.ToString());
|
||||
}
|
||||
public KeyPath GetWalletKeyPathRoot(PaymentMethodId paymentMethodId)
|
||||
{
|
||||
if (WalletKeyPathRoots.TryGetValue(paymentMethodId.ToString().ToLowerInvariant(), out var k))
|
||||
return KeyPath.Parse(k);
|
||||
return null;
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
[Obsolete("Use SetWalletKeyPathRoot/GetWalletKeyPathRoot instead")]
|
||||
public Dictionary<string, string> WalletKeyPathRoots { get; set; } = new Dictionary<string, string>();
|
||||
|
||||
public IPaymentFilter GetExcludedPaymentMethods()
|
||||
{
|
||||
|
@ -8,6 +8,18 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public const string Created = "invoice_created";
|
||||
public const string ReceivedPayment = "invoice_receivedPayment";
|
||||
public const string MarkedCompleted = "invoice_markedComplete";
|
||||
public const string MarkedInvalid= "invoice_markedInvalid";
|
||||
public const string Expired= "invoice_expired";
|
||||
public const string ExpiredPaidPartial= "invoice_expiredPaidPartial";
|
||||
public const string PaidInFull= "invoice_paidInFull";
|
||||
public const string PaidAfterExpiration= "invoice_paidAfterExpiration";
|
||||
public const string FailedToConfirm= "invoice_failedToConfirm";
|
||||
public const string Confirmed= "invoice_confirmed";
|
||||
public const string Completed= "invoice_completed";
|
||||
|
||||
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
|
||||
{
|
||||
Invoice = invoice;
|
||||
@ -19,6 +31,8 @@ namespace BTCPayServer.Events
|
||||
public int EventCode { get; set; }
|
||||
public string Name { get; set; }
|
||||
|
||||
public PaymentEntity Payment { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Invoice {Invoice.Id} new event: {Name} ({EventCode})";
|
||||
|
@ -341,14 +341,14 @@ namespace BTCPayServer.HostedServices
|
||||
// we need to use the status in the event and not in the invoice. The invoice might now be in another status.
|
||||
if (invoice.FullNotifications)
|
||||
{
|
||||
if (e.Name == "invoice_expired" ||
|
||||
e.Name == "invoice_paidInFull" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_markedInvalid" ||
|
||||
e.Name == "invoice_markedComplete" ||
|
||||
e.Name == "invoice_failedToConfirm" ||
|
||||
e.Name == "invoice_completed" ||
|
||||
e.Name == "invoice_expiredPaidPartial"
|
||||
if (e.Name == InvoiceEvent.Expired ||
|
||||
e.Name == InvoiceEvent.PaidInFull ||
|
||||
e.Name == InvoiceEvent.FailedToConfirm ||
|
||||
e.Name == InvoiceEvent.MarkedInvalid ||
|
||||
e.Name == InvoiceEvent.MarkedCompleted ||
|
||||
e.Name == InvoiceEvent.FailedToConfirm ||
|
||||
e.Name == InvoiceEvent.Completed ||
|
||||
e.Name == InvoiceEvent.ExpiredPaidPartial
|
||||
)
|
||||
tasks.Add(Notify(invoice));
|
||||
}
|
||||
|
@ -66,10 +66,10 @@ namespace BTCPayServer.HostedServices
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, InvoiceEvent.Expired));
|
||||
invoice.Status = InvoiceStatus.Expired;
|
||||
if(invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, InvoiceEvent.ExpiredPaidPartial));
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
@ -84,7 +84,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (invoice.Status == InvoiceStatus.New)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, InvoiceEvent.PaidInFull));
|
||||
invoice.Status = InvoiceStatus.Paid;
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
@ -93,7 +93,7 @@ namespace BTCPayServer.HostedServices
|
||||
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, InvoiceEvent.PaidAfterExpiration));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
@ -139,14 +139,14 @@ namespace BTCPayServer.HostedServices
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, InvoiceEvent.FailedToConfirm));
|
||||
invoice.Status = InvoiceStatus.Invalid;
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, InvoiceEvent.Confirmed));
|
||||
invoice.Status = InvoiceStatus.Confirmed;
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -157,7 +157,7 @@ namespace BTCPayServer.HostedServices
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, InvoiceEvent.Completed));
|
||||
invoice.Status = InvoiceStatus.Complete;
|
||||
context.MarkDirty();
|
||||
}
|
||||
@ -247,13 +247,13 @@ namespace BTCPayServer.HostedServices
|
||||
}));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
if (b.Name == InvoiceEvent.Created)
|
||||
{
|
||||
Watch(b.Invoice.Id);
|
||||
await Wait(b.Invoice.Id);
|
||||
}
|
||||
|
||||
if (b.Name == "invoice_receivedPayment")
|
||||
if (b.Name == InvoiceEvent.ReceivedPayment)
|
||||
{
|
||||
Watch(b.Invoice.Id);
|
||||
}
|
||||
|
@ -59,6 +59,12 @@ namespace BTCPayServer.HostedServices
|
||||
settings.ConvertMultiplierToSpread = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.ConvertNetworkFeeProperty)
|
||||
{
|
||||
await ConvertNetworkFeeProperty();
|
||||
settings.ConvertNetworkFeeProperty = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -67,6 +73,26 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertNetworkFeeProperty()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
{
|
||||
foreach (var store in await ctx.Stores.ToArrayAsync())
|
||||
{
|
||||
var blob = store.GetStoreBlob();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (blob.NetworkFeeDisabled != null)
|
||||
{
|
||||
blob.NetworkFeeMode = blob.NetworkFeeDisabled.Value ? NetworkFeeMode.Never : NetworkFeeMode.Always;
|
||||
blob.NetworkFeeDisabled = null;
|
||||
store.SetStoreBlob(blob);
|
||||
}
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConvertMultiplierToSpread()
|
||||
{
|
||||
using (var ctx = _DBContextFactory.CreateContext())
|
||||
|
@ -38,7 +38,9 @@ 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;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
@ -73,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>();
|
||||
@ -132,6 +135,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<IHostedService, HostedServices.CheckConfigurationHostedService>();
|
||||
|
||||
services.AddSingleton<Payments.IPaymentMethodHandler<Payments.Lightning.LightningSupportedPaymentMethod>, Payments.Lightning.LightningLikePaymentHandler>();
|
||||
services.AddSingleton<LightningLikePaymentHandler>();
|
||||
services.AddSingleton<IHostedService, Payments.Lightning.LightningListener>();
|
||||
|
||||
services.AddSingleton<ChangellyClientProvider>();
|
||||
@ -156,6 +160,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddScoped<IHttpContextAccessor, HttpContextAccessor>();
|
||||
services.AddTransient<AccessTokenController>();
|
||||
services.AddTransient<InvoiceController>();
|
||||
services.AddTransient<AppsPublicController>();
|
||||
// Add application services.
|
||||
services.AddTransient<IEmailSender, EmailSender>();
|
||||
// bundling
|
||||
|
@ -38,6 +38,7 @@ using Microsoft.Extensions.Options;
|
||||
using Microsoft.AspNetCore.Mvc.Cors.Internal;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using System.Net;
|
||||
using BTCPayServer.Hubs;
|
||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||
using BTCPayServer.Security;
|
||||
|
||||
@ -78,7 +79,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddIdentity<ApplicationUser, IdentityRole>()
|
||||
.AddEntityFrameworkStores<ApplicationDbContext>()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
services.AddSignalR();
|
||||
services.AddBTCPayServer();
|
||||
services.AddMvc(o =>
|
||||
{
|
||||
@ -99,7 +100,7 @@ namespace BTCPayServer.Hosting
|
||||
services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequiredLength = 7;
|
||||
options.Password.RequiredLength = 6;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
@ -198,6 +199,10 @@ namespace BTCPayServer.Hosting
|
||||
AppPath = options.GetRootUri(),
|
||||
Authorization = new[] { new NeedRole(Roles.ServerAdmin) }
|
||||
});
|
||||
app.UseSignalR(route =>
|
||||
{
|
||||
route.MapHub<CrowdfundHub>("/apps/crowdfund/hub");
|
||||
});
|
||||
app.UseWebSockets();
|
||||
app.UseStatusCodePages();
|
||||
app.UseMvc(routes =>
|
||||
|
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
public class UpdateCrowdfundViewModel
|
||||
{
|
||||
[Required] [MaxLength(30)] public string Title { get; set; }
|
||||
|
||||
[MaxLength(50)] public string Tagline { get; set; }
|
||||
|
||||
[Required] public string Description { get; set; }
|
||||
|
||||
[Display(Name = "Featured Image")]
|
||||
public string MainImageUrl { get; set; }
|
||||
|
||||
public string NotificationUrl { get; set; }
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Allow crowdfund to be publicly visible (still visible to you)")]
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Enable background animations on new payments")]
|
||||
public bool AnimationsEnabled { get; set; } = true;
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Enable sounds on new payments")]
|
||||
public bool SoundsEnabled { get; set; } = true;
|
||||
|
||||
[Required]
|
||||
[Display(Name = "Enable Disqus Comments")]
|
||||
public bool DisqusEnabled { get; set; } = true;
|
||||
|
||||
[Display(Name = "Disqus Shortname")] public string DisqusShortname { get; set; }
|
||||
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
[Required]
|
||||
[MaxLength(5)]
|
||||
[Display(Name = "The primary currency used for targets and stats. (e.g. BTC, LTC, USD, etc.)")]
|
||||
public string TargetCurrency { get; set; } = "BTC";
|
||||
|
||||
[Display(Name = "Set a Target amount ")]
|
||||
public decimal? TargetAmount { get; set; }
|
||||
|
||||
|
||||
public IEnumerable<string> ResetEveryValues = Enum.GetNames(typeof(CrowdfundResetEvery));
|
||||
|
||||
[Display(Name = "Reset goal every")] public string ResetEvery { get; set; } = nameof(CrowdfundResetEvery.Never);
|
||||
|
||||
public int ResetEveryAmount { get; set; } = 1;
|
||||
|
||||
|
||||
[Display(Name = "Do not allow additional contributions after target has been reached")]
|
||||
public bool EnforceTargetAmount { get; set; }
|
||||
|
||||
[Display(Name = "Contribution Perks Template")]
|
||||
public string PerksTemplate { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
[Display(Name = "Custom bootstrap CSS file")]
|
||||
public string CustomCSSLink { get; set; }
|
||||
|
||||
[Display(Name = "Custom CSS Code")]
|
||||
public string EmbeddedCSS { get; set; }
|
||||
|
||||
[Display(Name = "Base the contributed goal amount on the invoice amount and not actual payments")]
|
||||
public bool UseInvoiceAmount { get; set; }
|
||||
[Display(Name = "Count all invoices created on the store as part of the crowdfunding goal")]
|
||||
public bool UseAllStoreInvoices { get; set; }
|
||||
|
||||
public string AppId { get; set; }
|
||||
[Display(Name = "Sort contribution perks by popularity")]
|
||||
public bool SortPerksByPopularity { get; set; }
|
||||
[Display(Name = "Display contribution ranking")]
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
}
|
||||
|
||||
public enum CrowdfundResetEvery
|
||||
{
|
||||
Never,
|
||||
Hour,
|
||||
Day,
|
||||
Month,
|
||||
Year
|
||||
}
|
||||
}
|
@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
@ -36,8 +32,11 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public string CustomButtonText { get; set; }
|
||||
[Required]
|
||||
[MaxLength(30)]
|
||||
[Display(Name = "Do you want to leave a tip?")]
|
||||
[Display(Name = "Text to display in the tip input")]
|
||||
public string CustomTipText { get; set; }
|
||||
[MaxLength(30)]
|
||||
[Display(Name = "Tip percentage amounts (comma separated)")]
|
||||
public string CustomTipPercentages { get; set; }
|
||||
|
||||
[MaxLength(500)]
|
||||
[Display(Name = "Custom bootstrap CSS file")]
|
||||
|
69
BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs
Normal file
69
BTCPayServer/Models/AppViewModels/ViewCrowdfundViewModel.cs
Normal file
@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
namespace BTCPayServer.Models.AppViewModels
|
||||
{
|
||||
public class ViewCrowdfundViewModel
|
||||
{
|
||||
public string StatusMessage{ get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public string AppId { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string MainImageUrl { get; set; }
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public string CustomCSSLink { get; set; }
|
||||
public DateTime? StartDate { get; set; }
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
public string TargetCurrency { get; set; }
|
||||
public decimal? TargetAmount { get; set; }
|
||||
public bool EnforceTargetAmount { get; set; }
|
||||
|
||||
public CrowdfundInfo Info { get; set; }
|
||||
public string Tagline { get; set; }
|
||||
public ViewPointOfSaleViewModel.Item[] Perks { get; set; }
|
||||
public bool DisqusEnabled { get; set; }
|
||||
public bool SoundsEnabled { get; set; }
|
||||
public string DisqusShortname { get; set; }
|
||||
public bool AnimationsEnabled { get; set; }
|
||||
public int ResetEveryAmount { get; set; }
|
||||
public string ResetEvery { get; set; }
|
||||
|
||||
public Dictionary<string, int> PerkCount { get; set; }
|
||||
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
|
||||
public class CrowdfundInfo
|
||||
{
|
||||
public int TotalContributors { get; set; }
|
||||
public decimal CurrentPendingAmount { get; set; }
|
||||
public decimal CurrentAmount { get; set; }
|
||||
public decimal? ProgressPercentage { get; set; }
|
||||
public decimal? PendingProgressPercentage { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public Dictionary<string, decimal> PaymentStats { get; set; }
|
||||
public Dictionary<string, decimal> PendingPaymentStats { get; set; }
|
||||
public DateTime? LastResetDate { get; set; }
|
||||
public DateTime? NextResetDate { get; set; }
|
||||
}
|
||||
|
||||
public bool Started => !StartDate.HasValue || DateTime.Now.ToUniversalTime() > StartDate;
|
||||
|
||||
public bool Ended => !EndDate.HasValue || DateTime.Now.ToUniversalTime() > EndDate;
|
||||
public bool DisplayPerksRanking { get; set; }
|
||||
}
|
||||
|
||||
public class ContributeToCrowdfund
|
||||
{
|
||||
public ViewCrowdfundViewModel ViewCrowdfundViewModel { get; set; }
|
||||
[Required] public decimal Amount { get; set; }
|
||||
public string Email { get; set; }
|
||||
public string ChoiceKey { get; set; }
|
||||
public bool RedirectToCheckout { get; set; }
|
||||
public string RedirectUrl { get; set; }
|
||||
}
|
||||
}
|
@ -45,6 +45,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public string ButtonText { get; set; }
|
||||
public string CustomButtonText { get; set; }
|
||||
public string CustomTipText { get; set; }
|
||||
public int[] CustomTipPercentages { get; set; }
|
||||
|
||||
public string CustomCSSLink { get; set; }
|
||||
}
|
||||
|
@ -40,6 +40,12 @@ namespace BTCPayServer.Models
|
||||
//{"facade":"pos/invoice","data":{,}}
|
||||
public class InvoiceResponse
|
||||
{
|
||||
[JsonIgnore]
|
||||
public string StoreId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
//"url":"https://test.bitpay.com/invoice?id=9saCHtp1zyPcNoi3rDdBu8"
|
||||
[JsonProperty("url")]
|
||||
public string Url
|
||||
|
@ -61,5 +61,9 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public string PeerInfo { get; set; }
|
||||
public string ChangellyMerchantId { get; set; }
|
||||
public decimal? ChangellyAmountDue { get; set; }
|
||||
|
||||
public bool CoinSwitchEnabled { get; set; }
|
||||
public string CoinSwitchMode { get; set; }
|
||||
public string CoinSwitchMerchantId { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
} = new List<(string KeyPath, string Address)>();
|
||||
|
||||
public string CryptoCode { get; set; }
|
||||
public string KeyPath { get; set; }
|
||||
[Display(Name = "Hint address")]
|
||||
public string HintAddress { get; set; }
|
||||
public bool Confirmation { get; set; }
|
||||
|
@ -25,5 +25,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string InternalLightningNode { get; internal set; }
|
||||
public bool SkipPortTest { get; set; }
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public string StoreId { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -82,8 +82,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Display(Name = "Add network fee to invoice (vary with mining fees)")]
|
||||
public bool NetworkFee
|
||||
[Display(Name = "Add additional fee (network fee) to invoice...")]
|
||||
public Data.NetworkFeeMode NetworkFeeMode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Payments.CoinSwitch;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
{
|
||||
public class UpdateCoinSwitchSettingsViewModel
|
||||
{
|
||||
public string MerchantId { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
[Display(Name = "Integration Mode")]
|
||||
public string Mode { get; set; } = "inline";
|
||||
|
||||
public List<SelectListItem> Modes { get; } = new List<SelectListItem>
|
||||
{
|
||||
new SelectListItem { Value = "popup", Text = "Open in a popup" },
|
||||
new SelectListItem { Value = "inline", Text = "Embed inside Checkout UI " },
|
||||
};
|
||||
|
||||
public string StatusMessage { get; set; }
|
||||
}
|
||||
}
|
@ -20,27 +20,21 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
return DepositAddress;
|
||||
}
|
||||
|
||||
public decimal GetTxFee()
|
||||
public decimal GetNextNetworkFee()
|
||||
{
|
||||
return TxFee.ToDecimal(MoneyUnit.BTC);
|
||||
return NextNetworkFee.ToDecimal(MoneyUnit.BTC);
|
||||
}
|
||||
|
||||
public void SetNoTxFee()
|
||||
{
|
||||
TxFee = Money.Zero;
|
||||
}
|
||||
|
||||
|
||||
public void SetPaymentDestination(string newPaymentDestination)
|
||||
{
|
||||
DepositAddress = newPaymentDestination;
|
||||
}
|
||||
public Data.NetworkFeeMode NetworkFeeMode { get; set; }
|
||||
|
||||
// Those properties are JsonIgnore because their data is inside CryptoData class for legacy reason
|
||||
[JsonIgnore]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
[JsonIgnore]
|
||||
public Money TxFee { get; set; }
|
||||
public Money NextNetworkFee { get; set; }
|
||||
[JsonIgnore]
|
||||
public String DepositAddress { get; set; }
|
||||
public BitcoinAddress GetDepositAddress(Network network)
|
||||
|
@ -32,6 +32,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
public TxOut Output { get; set; }
|
||||
public int ConfirmationCount { get; set; }
|
||||
public bool RBF { get; set; }
|
||||
public decimal NetworkFee { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// This is set to true if the payment was created before CryptoPaymentData existed in BTCPayServer
|
||||
|
@ -48,8 +48,18 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
var prepare = (Prepare)preparePaymentObject;
|
||||
Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod = new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
|
||||
onchainMethod.NetworkFeeMode = store.GetStoreBlob().NetworkFeeMode;
|
||||
onchainMethod.FeeRate = await prepare.GetFeeRate;
|
||||
onchainMethod.TxFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
|
||||
switch (onchainMethod.NetworkFeeMode)
|
||||
{
|
||||
case NetworkFeeMode.Always:
|
||||
onchainMethod.NextNetworkFee = onchainMethod.FeeRate.GetFee(100); // assume price for 100 bytes
|
||||
break;
|
||||
case NetworkFeeMode.Never:
|
||||
case NetworkFeeMode.MultiplePaymentsOnly:
|
||||
onchainMethod.NextNetworkFee = Money.Zero;
|
||||
break;
|
||||
}
|
||||
onchainMethod.DepositAddress = (await prepare.ReserveAddress).ToString();
|
||||
return onchainMethod;
|
||||
}
|
||||
|
@ -158,7 +158,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
var alreadyExist = GetAllBitcoinPaymentData(invoice).Where(c => c.GetPaymentId() == paymentData.GetPaymentId()).Any();
|
||||
if (!alreadyExist)
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network.CryptoCode);
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, paymentData, network);
|
||||
if(payment != null)
|
||||
await ReceivedPayment(wallet, invoice, payment, evt.DerivationStrategy);
|
||||
}
|
||||
@ -332,7 +332,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
{
|
||||
var transaction = await wallet.GetTransactionAsync(coin.Coin.Outpoint.Hash);
|
||||
var paymentData = new BitcoinLikePaymentData(coin.Coin, transaction.Transaction.RBF);
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network.CryptoCode).ConfigureAwait(false);
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.Timestamp, paymentData, network).ConfigureAwait(false);
|
||||
alreadyAccounted.Add(coin.Coin.Outpoint);
|
||||
if (payment != null)
|
||||
{
|
||||
@ -373,7 +373,7 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
invoice.SetPaymentMethod(paymentMethod);
|
||||
}
|
||||
wallet.InvalidateCache(strategy);
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
|
||||
return invoice;
|
||||
}
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
|
15
BTCPayServer/Payments/Coinswitch/CoinswitchSettings.cs
Normal file
15
BTCPayServer/Payments/Coinswitch/CoinswitchSettings.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace BTCPayServer.Payments.CoinSwitch
|
||||
{
|
||||
public class CoinSwitchSettings
|
||||
{
|
||||
public string MerchantId { get; set; }
|
||||
public string Mode { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public bool IsConfigured()
|
||||
{
|
||||
return
|
||||
!string.IsNullOrEmpty(MerchantId);
|
||||
}
|
||||
}
|
||||
}
|
@ -18,11 +18,10 @@ namespace BTCPayServer.Payments
|
||||
string GetPaymentDestination();
|
||||
PaymentTypes GetPaymentType();
|
||||
/// <summary>
|
||||
/// Returns what a merchant would need to pay to cashout this payment
|
||||
/// Returns fee that the merchant charge to the customer for the next payment
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
decimal GetTxFee();
|
||||
void SetNoTxFee();
|
||||
decimal GetNextNetworkFee();
|
||||
/// <summary>
|
||||
/// Change the payment destination (internal plumbing)
|
||||
/// </summary>
|
||||
|
@ -22,6 +22,8 @@ namespace BTCPayServer.Payments.Lightning
|
||||
return GetPaymentId();
|
||||
}
|
||||
|
||||
public decimal NetworkFee { get; set; }
|
||||
|
||||
public string GetPaymentId()
|
||||
{
|
||||
return BOLT11;
|
||||
|
@ -25,7 +25,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public override async Task<IPaymentMethodDetails> CreatePaymentMethodDetails(LightningSupportedPaymentMethod supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store, BTCPayNetwork network, object preparePaymentObject)
|
||||
{
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var test = Test(supportedPaymentMethod, network);
|
||||
var test = GetNodeInfo(supportedPaymentMethod, network);
|
||||
var invoice = paymentMethod.ParentEntity;
|
||||
var due = Extensions.RoundUp(invoice.ProductInformation.Price / paymentMethod.Rate, 8);
|
||||
var client = supportedPaymentMethod.CreateClient(network);
|
||||
@ -63,7 +63,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<NodeInfo> Test(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
public async Task<NodeInfo> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new PaymentMethodUnavailableException($"Full node not available");
|
||||
@ -78,7 +78,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
}
|
||||
catch (OperationCanceledException) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely maner");
|
||||
throw new PaymentMethodUnavailableException($"The lightning node did not replied in a timely manner");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
@ -21,15 +22,10 @@ namespace BTCPayServer.Payments.Lightning
|
||||
return PaymentTypes.LightningLike;
|
||||
}
|
||||
|
||||
public decimal GetTxFee()
|
||||
public decimal GetNextNetworkFee()
|
||||
{
|
||||
return 0.0m;
|
||||
}
|
||||
|
||||
public void SetNoTxFee()
|
||||
{
|
||||
}
|
||||
|
||||
public void SetPaymentDestination(string newPaymentDestination)
|
||||
{
|
||||
BOLT11 = newPaymentDestination;
|
||||
|
@ -42,7 +42,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
|
||||
{
|
||||
if (inv.Name == "invoice_created")
|
||||
if (inv.Name == InvoiceEvent.Created)
|
||||
{
|
||||
await EnsureListening(inv.Invoice.Id, false);
|
||||
}
|
||||
@ -192,12 +192,12 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
BOLT11 = notification.BOLT11,
|
||||
Amount = notification.Amount
|
||||
}, network.CryptoCode, accounted: true);
|
||||
}, network, accounted: true);
|
||||
if (payment != null)
|
||||
{
|
||||
var invoice = await _InvoiceRepository.GetInvoice(listenedInvoice.InvoiceId);
|
||||
if (invoice != null)
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, "invoice_receivedPayment"));
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1002, InvoiceEvent.ReceivedPayment){Payment = payment});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
"BTCPAY_BTCEXTERNALLNDGRPC": "type=lnd-grpc;server=https://lnd:lnd@127.0.0.1:53280/;allowinsecure=true",
|
||||
"BTCPAY_BTCEXTERNALLNDREST": "type=lnd-rest;server=https://lnd:lnd@127.0.0.1:53280/lnd-rest/btc/;allowinsecure=true;macaroonfilepath=D:\\admin.macaroon",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver"
|
||||
@ -32,6 +33,7 @@
|
||||
"BTCPAY_BTCEXTERNALSPARK": "server=https://127.0.0.1:53280/spark/btc/;cookiefile=fake",
|
||||
"BTCPAY_BTCEXTERNALCHARGE": "server=https://127.0.0.1:53280/spark/btc/;cookiefilepath=fake",
|
||||
"BTCPAY_BTCEXPLORERURL": "http://127.0.0.1:32838/",
|
||||
"BTCPAY_DISABLE-REGISTRATION": "false",
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"BTCPAY_CHAINS": "btc,ltc",
|
||||
"BTCPAY_POSTGRES": "User ID=postgres;Host=127.0.0.1;Port=39372;Database=btcpayserver",
|
||||
|
@ -7,6 +7,7 @@ namespace BTCPayServer.Services.Apps
|
||||
{
|
||||
public enum AppType
|
||||
{
|
||||
PointOfSale
|
||||
PointOfSale,
|
||||
Crowdfund
|
||||
}
|
||||
}
|
||||
|
@ -89,20 +89,19 @@ namespace BTCPayServer.Services
|
||||
return new LedgerTestResult() { Success = true };
|
||||
}
|
||||
|
||||
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, int account, CancellationToken cancellation)
|
||||
public async Task<GetXPubResult> GetExtPubKey(BTCPayNetwork network, KeyPath keyPath, CancellationToken cancellation)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
|
||||
var segwit = network.NBitcoinNetwork.Consensus.SupportSegwit;
|
||||
var path = network.GetRootKeyPath().Derive(account, true);
|
||||
var pubkey = await GetExtPubKey(Ledger, network, path, false, cancellation);
|
||||
var pubkey = await GetExtPubKey(Ledger, network, keyPath, false, cancellation);
|
||||
var derivation = new DerivationStrategyFactory(network.NBitcoinNetwork).CreateDirectDerivationStrategy(pubkey, new DerivationStrategyOptions()
|
||||
{
|
||||
P2SH = segwit,
|
||||
Legacy = !segwit
|
||||
});
|
||||
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = path };
|
||||
return new GetXPubResult() { ExtPubKey = derivation.ToString(), KeyPath = keyPath };
|
||||
}
|
||||
|
||||
private static async Task<BitcoinExtPubKey> GetExtPubKey(LedgerClient ledger, BTCPayNetwork network, KeyPath account, bool onlyChaincode, CancellationToken cancellation)
|
||||
@ -129,7 +128,13 @@ namespace BTCPayServer.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<KeyPath> GetKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
|
||||
public async Task<bool> CanSign(BTCPayNetwork network, DirectDerivationStrategy strategy, KeyPath keyPath, CancellationToken cancellation)
|
||||
{
|
||||
var hwKey = await GetExtPubKey(Ledger, network, keyPath, true, cancellation);
|
||||
return hwKey.ExtPubKey.PubKey == strategy.Root.PubKey;
|
||||
}
|
||||
|
||||
public async Task<KeyPath> FindKeyPath(BTCPayNetwork network, DirectDerivationStrategy directStrategy, CancellationToken cancellation)
|
||||
{
|
||||
List<KeyPath> derivations = new List<KeyPath>();
|
||||
if (network.NBitcoinNetwork.Consensus.SupportSegwit)
|
||||
@ -164,7 +169,17 @@ namespace BTCPayServer.Services
|
||||
KeyPath changeKeyPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
||||
try
|
||||
{
|
||||
var signedTransaction = await Ledger.SignTransactionAsync(signatureRequests, unsigned, changeKeyPath, cancellationToken);
|
||||
if (signedTransaction == null)
|
||||
throw new Exception("The ledger failed to sign the transaction");
|
||||
return signedTransaction;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception("The ledger failed to sign the transaction", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@ -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,17 +55,21 @@ 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
|
||||
foreach (var payment in invoice.GetPayments())
|
||||
{
|
||||
// not accounted payments are payments which got double spent like RBfed
|
||||
if (!payment.Accounted)
|
||||
continue;
|
||||
|
||||
var cryptoCode = payment.GetPaymentMethodId().CryptoCode;
|
||||
var pdata = payment.GetCryptoPaymentData();
|
||||
|
||||
var pmethod = invoice.GetPaymentMethod(payment.GetPaymentMethodId(), Networks);
|
||||
var paidAfterNetworkFees = pdata.GetValue() - payment.NetworkFee;
|
||||
invoiceDue -= paidAfterNetworkFees * pmethod.Rate;
|
||||
|
||||
var target = new ExportInvoiceHolder
|
||||
{
|
||||
@ -73,6 +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 = 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 = Math.Round(invoiceDue, currency.NumberDecimalDigits),
|
||||
OrderId = invoice.OrderId,
|
||||
StoreId = invoice.StoreId,
|
||||
InvoiceId = invoice.Id,
|
||||
@ -112,12 +126,14 @@ namespace BTCPayServer.Services.Invoices.Export
|
||||
public string PaymentId { get; set; }
|
||||
public string Destination { get; set; }
|
||||
public string PaymentType { get; set; }
|
||||
public string Paid { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
public string Paid { get; set; }
|
||||
public string NetworkFee { get; set; }
|
||||
public decimal ConversionRate { get; set; }
|
||||
|
||||
public decimal InvoicePrice { get; set; }
|
||||
public string PaidCurrency { get; set; }
|
||||
public string InvoiceCurrency { get; set; }
|
||||
public decimal InvoiceDue { get; set; }
|
||||
public decimal InvoicePrice { get; set; }
|
||||
public string InvoiceItemCode { get; set; }
|
||||
public string InvoiceItemDesc { get; set; }
|
||||
public string InvoiceFullStatus { get; set; }
|
||||
|
@ -344,6 +344,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
InvoiceResponse dto = new InvoiceResponse
|
||||
{
|
||||
Id = Id,
|
||||
StoreId = StoreId,
|
||||
OrderId = OrderId,
|
||||
PosData = PosData,
|
||||
CurrentTime = DateTimeOffset.UtcNow,
|
||||
@ -485,7 +486,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
public PaymentMethodDictionary GetPaymentMethods(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
PaymentMethodDictionary rates = new PaymentMethodDictionary(networkProvider);
|
||||
PaymentMethodDictionary paymentMethods = new PaymentMethodDictionary();
|
||||
var serializer = new Serializer(Dummy);
|
||||
#pragma warning disable CS0618
|
||||
if (PaymentMethod != null)
|
||||
@ -499,11 +500,11 @@ namespace BTCPayServer.Services.Invoices
|
||||
r.ParentEntity = this;
|
||||
r.Network = networkProvider?.GetNetwork(r.CryptoCode);
|
||||
if (r.Network != null || networkProvider == null)
|
||||
rates.Add(r);
|
||||
paymentMethods.Add(r);
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
return rates;
|
||||
return paymentMethods;
|
||||
}
|
||||
|
||||
Network Dummy = Network.Main;
|
||||
@ -517,8 +518,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
public void SetPaymentMethods(PaymentMethodDictionary paymentMethods)
|
||||
{
|
||||
if (paymentMethods.NetworkProvider != null)
|
||||
throw new InvalidOperationException($"{nameof(paymentMethods)} should have NetworkProvider to null");
|
||||
var obj = new JObject();
|
||||
var serializer = new Serializer(Dummy);
|
||||
#pragma warning disable CS0618
|
||||
@ -681,7 +680,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
/// </summary>
|
||||
public Money NetworkFee { get; set; }
|
||||
/// <summary>
|
||||
/// Minimum required to be paid in order to accept invocie as paid
|
||||
/// Minimum required to be paid in order to accept invoice as paid
|
||||
/// </summary>
|
||||
public Money MinimumTotalDue { get; set; }
|
||||
}
|
||||
@ -731,7 +730,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
{
|
||||
FeeRate = FeeRate,
|
||||
DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress,
|
||||
TxFee = TxFee
|
||||
NextNetworkFee = NextNetworkFee
|
||||
};
|
||||
}
|
||||
else
|
||||
@ -739,7 +738,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
var details = PaymentMethodExtensions.DeserializePaymentMethodDetails(GetId(), PaymentMethodDetails);
|
||||
if (details is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod btcLike)
|
||||
{
|
||||
btcLike.TxFee = TxFee;
|
||||
btcLike.NextNetworkFee = NextNetworkFee;
|
||||
btcLike.DepositAddress = string.IsNullOrEmpty(DepositAddress) ? null : DepositAddress;
|
||||
btcLike.FeeRate = FeeRate;
|
||||
}
|
||||
@ -761,7 +760,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
if (paymentMethod is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod)
|
||||
{
|
||||
TxFee = bitcoinPaymentMethod.TxFee;
|
||||
NextNetworkFee = bitcoinPaymentMethod.NextNetworkFee;
|
||||
FeeRate = bitcoinPaymentMethod.FeeRate;
|
||||
DepositAddress = bitcoinPaymentMethod.DepositAddress;
|
||||
}
|
||||
@ -776,8 +775,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).FeeRate")]
|
||||
public FeeRate FeeRate { get; set; }
|
||||
[JsonProperty(PropertyName = "txFee")]
|
||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).TxFee")]
|
||||
public Money TxFee { get; set; }
|
||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).NextNetworkFee")]
|
||||
public Money NextNetworkFee { get; set; }
|
||||
[JsonProperty(PropertyName = "depositAddress")]
|
||||
[Obsolete("Use ((BitcoinLikeOnChainPaymentMethod)GetPaymentMethod()).DepositAddress")]
|
||||
public string DepositAddress { get; set; }
|
||||
@ -801,7 +800,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
.OrderBy(p => p.ReceivedTime)
|
||||
.Select(_ =>
|
||||
{
|
||||
var txFee = _.GetValue(paymentMethods, GetId(), paymentMethods[_.GetPaymentMethodId()].GetTxFee());
|
||||
var txFee = _.GetValue(paymentMethods, GetId(), _.NetworkFee);
|
||||
paid += _.GetValue(paymentMethods, GetId());
|
||||
if (!paidEnough)
|
||||
{
|
||||
@ -842,17 +841,18 @@ namespace BTCPayServer.Services.Invoices
|
||||
var method = GetPaymentMethodDetails();
|
||||
if (method == null)
|
||||
return 0.0m;
|
||||
return method.GetTxFee();
|
||||
return method.GetNextNetworkFee();
|
||||
}
|
||||
}
|
||||
|
||||
public class PaymentEntity
|
||||
{
|
||||
public int Version { get; set; }
|
||||
public DateTimeOffset ReceivedTime
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public decimal NetworkFee { get; set; }
|
||||
[Obsolete("Use ((BitcoinLikePaymentData)GetCryptoPaymentData()).Outpoint")]
|
||||
public OutPoint Outpoint
|
||||
{
|
||||
@ -889,7 +889,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
#pragma warning disable CS0618
|
||||
if (string.IsNullOrEmpty(CryptoPaymentDataType))
|
||||
{
|
||||
// In case this is a payment done before this update, consider it unconfirmed with RBF for safety
|
||||
// For invoices created when CryptoPaymentDataType was not existing, we just consider that it is a RBFed payment for safety
|
||||
var paymentData = new Payments.Bitcoin.BitcoinLikePaymentData();
|
||||
paymentData.Outpoint = Outpoint;
|
||||
paymentData.Output = Output;
|
||||
|
@ -41,7 +41,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
public InvoiceRepository(ApplicationDbContextFactory contextFactory, string dbreezePath)
|
||||
{
|
||||
int retryCount = 0;
|
||||
retry:
|
||||
retry:
|
||||
try
|
||||
{
|
||||
_Engine = new DBreezeEngine(dbreezePath);
|
||||
@ -193,7 +193,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
return paymentMethod.GetPaymentMethodDetails().GetPaymentDestination();
|
||||
}
|
||||
|
||||
public async Task<bool> NewAddress(string invoiceId, IPaymentMethodDetails paymentMethod, BTCPayNetwork network)
|
||||
public async Task<bool> NewAddress(string invoiceId, Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod paymentMethod, BTCPayNetwork network)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
@ -206,14 +206,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (currencyData == null)
|
||||
return false;
|
||||
|
||||
var existingPaymentMethod = currencyData.GetPaymentMethodDetails();
|
||||
var existingPaymentMethod = (Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)currencyData.GetPaymentMethodDetails();
|
||||
if (existingPaymentMethod.GetPaymentDestination() != null)
|
||||
{
|
||||
MarkUnassigned(invoiceId, invoiceEntity, context, currencyData.GetId());
|
||||
}
|
||||
|
||||
existingPaymentMethod.SetPaymentDestination(paymentMethod.GetPaymentDestination());
|
||||
|
||||
currencyData.SetPaymentMethodDetails(existingPaymentMethod);
|
||||
#pragma warning disable CS0618
|
||||
if (network.IsBTC)
|
||||
@ -379,13 +378,27 @@ namespace BTCPayServer.Services.Invoices
|
||||
private InvoiceEntity ToEntity(Data.InvoiceData invoice)
|
||||
{
|
||||
var entity = ToObject<InvoiceEntity>(invoice.Blob, null);
|
||||
PaymentMethodDictionary paymentMethods = null;
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = invoice.Payments.Select(p =>
|
||||
{
|
||||
var paymentEntity = ToObject<PaymentEntity>(p.Blob, null);
|
||||
paymentEntity.Accounted = p.Accounted;
|
||||
|
||||
// PaymentEntity on version 0 does not have their own fee, because it was assumed that the payment method have fixed fee.
|
||||
// We want to hide this legacy detail in InvoiceRepository, so we fetch the fee from the PaymentMethod and assign it to the PaymentEntity.
|
||||
if (paymentEntity.Version == 0)
|
||||
{
|
||||
if (paymentMethods == null)
|
||||
paymentMethods = entity.GetPaymentMethods(null);
|
||||
var paymentMethodDetails = paymentMethods.TryGet(paymentEntity.GetPaymentMethodId())?.GetPaymentMethodDetails();
|
||||
if (paymentMethodDetails != null) // == null should never happen, but we never know.
|
||||
paymentEntity.NetworkFee = paymentMethodDetails.GetNextNetworkFee();
|
||||
}
|
||||
|
||||
return paymentEntity;
|
||||
}).ToList();
|
||||
})
|
||||
.OrderBy(a => a.ReceivedTime).ToList();
|
||||
#pragma warning restore CS0618
|
||||
var state = invoice.GetInvoiceState();
|
||||
entity.ExceptionStatus = state.ExceptionStatus;
|
||||
@ -450,11 +463,16 @@ namespace BTCPayServer.Services.Invoices
|
||||
if (queryObject.EndDate != null)
|
||||
query = query.Where(i => i.Created <= queryObject.EndDate.Value);
|
||||
|
||||
if (queryObject.ItemCode != null)
|
||||
query = query.Where(i => i.ItemCode == queryObject.ItemCode);
|
||||
|
||||
if (queryObject.OrderId != null)
|
||||
query = query.Where(i => i.OrderId == queryObject.OrderId);
|
||||
if (queryObject.OrderId != null && queryObject.OrderId.Length > 0)
|
||||
{
|
||||
var statusSet = queryObject.OrderId.ToHashSet();
|
||||
query = query.Where(i => statusSet.Contains(i.OrderId));
|
||||
}
|
||||
if (queryObject.ItemCode != null && queryObject.ItemCode.Length > 0)
|
||||
{
|
||||
var statusSet = queryObject.ItemCode.ToHashSet();
|
||||
query = query.Where(i => statusSet.Contains(i.ItemCode));
|
||||
}
|
||||
|
||||
if (queryObject.Status != null && queryObject.Status.Length > 0)
|
||||
{
|
||||
@ -546,21 +564,37 @@ namespace BTCPayServer.Services.Invoices
|
||||
/// <param name="cryptoCode"></param>
|
||||
/// <param name="accounted"></param>
|
||||
/// <returns>The PaymentEntity or null if already added</returns>
|
||||
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, string cryptoCode, bool accounted = false)
|
||||
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, CryptoPaymentData paymentData, BTCPayNetwork network, bool accounted = false)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
var invoice = context.Invoices.Find(invoiceId);
|
||||
if (invoice == null)
|
||||
return null;
|
||||
InvoiceEntity invoiceEntity = ToObject<InvoiceEntity>(invoice.Blob, network.NBitcoinNetwork);
|
||||
PaymentMethod paymentMethod = invoiceEntity.GetPaymentMethod(new PaymentMethodId(network.CryptoCode, paymentData.GetPaymentType()), null);
|
||||
IPaymentMethodDetails paymentMethodDetails = paymentMethod.GetPaymentMethodDetails();
|
||||
PaymentEntity entity = new PaymentEntity
|
||||
{
|
||||
Version = 1,
|
||||
#pragma warning disable CS0618
|
||||
CryptoCode = cryptoCode,
|
||||
CryptoCode = network.CryptoCode,
|
||||
#pragma warning restore CS0618
|
||||
ReceivedTime = date.UtcDateTime,
|
||||
Accounted = accounted
|
||||
Accounted = accounted,
|
||||
NetworkFee = paymentMethodDetails.GetNextNetworkFee()
|
||||
};
|
||||
entity.SetCryptoPaymentData(paymentData);
|
||||
|
||||
|
||||
if (paymentMethodDetails is Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod bitcoinPaymentMethod &&
|
||||
bitcoinPaymentMethod.NetworkFeeMode == NetworkFeeMode.MultiplePaymentsOnly &&
|
||||
bitcoinPaymentMethod.NextNetworkFee == Money.Zero)
|
||||
{
|
||||
bitcoinPaymentMethod.NextNetworkFee = bitcoinPaymentMethod.FeeRate.GetFee(100); // assume price for 100 bytes
|
||||
paymentMethod.SetPaymentMethodDetails(bitcoinPaymentMethod);
|
||||
invoiceEntity.SetPaymentMethod(paymentMethod);
|
||||
invoice.Blob = ToBytes(invoiceEntity, network.NBitcoinNetwork);
|
||||
}
|
||||
PaymentData data = new PaymentData
|
||||
{
|
||||
Id = paymentData.GetPaymentId(),
|
||||
@ -665,12 +699,12 @@ namespace BTCPayServer.Services.Invoices
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string OrderId
|
||||
public string[] OrderId
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string ItemCode
|
||||
public string[] ItemCode
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
@ -15,13 +15,6 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
}
|
||||
|
||||
public PaymentMethodDictionary(BTCPayNetworkProvider networkProvider)
|
||||
{
|
||||
NetworkProvider = networkProvider;
|
||||
}
|
||||
|
||||
|
||||
public BTCPayNetworkProvider NetworkProvider { get; set; }
|
||||
public PaymentMethod this[PaymentMethodId index]
|
||||
{
|
||||
get
|
||||
|
@ -10,5 +10,6 @@ namespace BTCPayServer.Services
|
||||
public bool UnreachableStoreCheck { get; set; }
|
||||
public bool DeprecatedLightningConnectionStringCheck { get; set; }
|
||||
public bool ConvertMultiplierToSpread { get; set; }
|
||||
public bool ConvertNetworkFeeProperty { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -162,6 +162,8 @@ namespace BTCPayServer.Services.Stores
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
throw new ArgumentException("name should not be empty", nameof(name));
|
||||
if (ownerId == null)
|
||||
throw new ArgumentNullException(nameof(ownerId));
|
||||
using (var ctx = _ContextFactory.CreateContext())
|
||||
{
|
||||
StoreData store = new StoreData
|
||||
|
243
BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml
Normal file
243
BTCPayServer/Views/Apps/UpdateCrowdfund.cshtml
Normal file
@ -0,0 +1,243 @@
|
||||
@using BTCPayServer.Hubs
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@model UpdateCrowdfundViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Update Crowdfund";
|
||||
}
|
||||
<section>
|
||||
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Contribution Perks Management</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<h2 class="section-heading">@ViewData["Title"]</h2>
|
||||
<hr class="primary">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<form method="post">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Title" class="control-label"></label>*
|
||||
<input asp-for="Title" class="form-control" />
|
||||
<span asp-validation-for="Title" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Tagline" class="control-label"></label>
|
||||
<input asp-for="Tagline" class="form-control" />
|
||||
<span asp-validation-for="Tagline" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Description" class="control-label"></label>
|
||||
<textarea asp-for="Description" rows="20" cols="40" class="form-control richtext"></textarea>
|
||||
<span asp-validation-for="Description" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="TargetCurrency" class="control-label"></label>
|
||||
<input asp-for="TargetCurrency" class="form-control" />
|
||||
<span asp-validation-for="TargetCurrency" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="TargetAmount" class="control-label"></label>
|
||||
<input asp-for="TargetAmount" class="form-control" />
|
||||
<span asp-validation-for="TargetAmount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="StartDate" class="control-label"></label>
|
||||
<input asp-for="StartDate" class="form-control datetime " />
|
||||
<span asp-validation-for="StartDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="ResetEvery" class="control-label"></label>
|
||||
<div class="input-group">
|
||||
|
||||
<input type="number" asp-for="ResetEveryAmount" placeholder="Amount" class="form-control">
|
||||
<select class="custom-select" asp-for="ResetEvery">
|
||||
@foreach (var opt in Model.ResetEveryValues)
|
||||
{
|
||||
<option value="@opt">@opt</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="EndDate" class="control-label"></label>
|
||||
<input asp-for="EndDate" class="form-control datetime" />
|
||||
<span asp-validation-for="EndDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Contribution Perks</label>
|
||||
<div class="mb-3">
|
||||
<a class="js-product-add btn btn-secondary" href="#" data-toggle="modal" data-target="#product-modal"><i class="fa fa-plus fa-fw"></i> Add Perk</a>
|
||||
</div>
|
||||
<div class="js-products bg-light row p-3">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PerksTemplate" class="control-label"></label>
|
||||
<textarea asp-for="PerksTemplate" rows="10" cols="40" class="js-product-template form-control"></textarea>
|
||||
<span asp-validation-for="PerksTemplate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="control-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="MainImageUrl" class="control-label"></label>
|
||||
<input asp-for="MainImageUrl" class="form-control" />
|
||||
<span asp-validation-for="MainImageUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="EmbeddedCSS" class="control-label"></label>
|
||||
|
||||
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
|
||||
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="NotificationUrl" class="control-label"></label>
|
||||
<input asp-for="NotificationUrl" class="form-control" />
|
||||
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Enabled"></label>
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="Enabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SortPerksByPopularity"></label>
|
||||
<input asp-for="SortPerksByPopularity" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="SortPerksByPopularity" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DisplayPerksRanking"></label>
|
||||
<input asp-for="DisplayPerksRanking" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="DisplayPerksRanking" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="EnforceTargetAmount"></label>
|
||||
<input asp-for="EnforceTargetAmount" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="EnforceTargetAmount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="UseInvoiceAmount"></label>
|
||||
<input asp-for="UseInvoiceAmount" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="UseInvoiceAmount" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="UseAllStoreInvoices"></label>
|
||||
<input asp-for="UseAllStoreInvoices" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="UseAllStoreInvoices" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="SoundsEnabled"></label>
|
||||
<input asp-for="SoundsEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="SoundsEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AnimationsEnabled"></label>
|
||||
<input asp-for="AnimationsEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="AnimationsEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DisqusEnabled"></label>
|
||||
<input asp-for="DisqusEnabled" type="checkbox" class="form-check"/>
|
||||
<span asp-validation-for="DisqusEnabled" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="DisqusShortname" class="control-label"></label>
|
||||
<input asp-for="DisqusShortname" class="form-control" />
|
||||
<span asp-validation-for="DisqusShortname" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="submit" class="btn btn-primary" value="Save Settings" />
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{Model.AppId}")">Invoices generated by app</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ViewCrowdfund" asp-controller="AppsPublic" asp-route-appId="@Model.AppId">View App</a>
|
||||
<a class="btn btn-secondary" target="_blank" asp-action="ListApps">Back to the app list</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
|
||||
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.css"></bundle>
|
||||
|
||||
<script id="template-product-item" type="text/template">
|
||||
<div class="col-sm-4 col-md-3 mb-3">
|
||||
<div class="card">
|
||||
{image}
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{title}</h6>
|
||||
<a href="#" class="js-product-edit btn btn-primary" data-toggle="modal" data-target="#product-modal">Edit</a>
|
||||
<a href="#" class="js-product-remove btn btn-danger"><i class="fa fa-trash"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script id="template-product-content" type="text/template">
|
||||
<div class="mb-3">
|
||||
<input class="js-product-id" type="hidden" name="id" value="{id}">
|
||||
<input class="js-product-index" type="hidden" name="index" value="{index}">
|
||||
<div class="form-row">
|
||||
<div class="col-sm-6">
|
||||
<label>Title</label>*
|
||||
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Price</label>*
|
||||
<input type="number" step="any" class="js-product-price form-control mb-2" value="{price}" />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Custom price</label>
|
||||
<select class="js-product-custom form-control">
|
||||
{custom}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Image</label>
|
||||
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Description</label>
|
||||
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
}
|
||||
|
@ -72,6 +72,11 @@
|
||||
<input asp-for="CustomTipText" class="form-control" />
|
||||
<span asp-validation-for="CustomTipText" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomTipPercentages" class="control-label"></label>
|
||||
<input asp-for="CustomTipPercentages" class="form-control" />
|
||||
<span asp-validation-for="CustomTipPercentages" 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>
|
||||
@ -154,7 +159,7 @@
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Price</label>*
|
||||
<input type="text" class="js-product-price form-control mb-2" value="{price}" />
|
||||
<input type="number" class="js-product-price form-control mb-2" value="{price}" />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Custom price</label>
|
||||
|
@ -0,0 +1,78 @@
|
||||
@using Microsoft.EntityFrameworkCore.Internal
|
||||
@model BTCPayServer.Models.AppViewModels.ContributeToCrowdfund
|
||||
|
||||
<form method="post">
|
||||
@foreach (var item in Model.ViewCrowdfundViewModel.Perks)
|
||||
{
|
||||
<div class="card mb-4 perk expanded" id="@item.Id">
|
||||
@if (Model.ViewCrowdfundViewModel.DisplayPerksRanking && Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))
|
||||
{
|
||||
<span class="btn btn-sm rounded-circle px-0 btn-primary perk-badge">#@(Model.ViewCrowdfundViewModel.Perks.IndexOf(item)+1)</span>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(item.Image))
|
||||
{
|
||||
<img class="card-img-top" src="@item.Image"/>
|
||||
}
|
||||
<div class="card-body">
|
||||
<div class="card-title d-flex justify-content-between">
|
||||
<label class="h5">
|
||||
|
||||
@if (Model.ViewCrowdfundViewModel.Started && !Model.ViewCrowdfundViewModel.Ended && (item.Price.Value > 0 || item.Custom))
|
||||
{
|
||||
|
||||
<input type="radio" asp-for="ChoiceKey" value="@item.Id"/>
|
||||
}
|
||||
@(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title)
|
||||
</label>
|
||||
<span class="text-muted">
|
||||
@if (item.Price.Value > 0)
|
||||
{
|
||||
@item.Price.Value
|
||||
if (item.Custom)
|
||||
{
|
||||
Html.Raw("or more");
|
||||
}
|
||||
}
|
||||
else if (item.Custom)
|
||||
{
|
||||
Html.Raw("Any amount");
|
||||
}
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<p class="card-text overflow-hidden">@Html.Raw(item.Description)</p>
|
||||
|
||||
</div>
|
||||
@if (Model.ViewCrowdfundViewModel.PerkCount.ContainsKey(item.Id))
|
||||
{
|
||||
<div class="card-footer d-flex justify-content-between">
|
||||
<span></span>
|
||||
<span> @Model.ViewCrowdfundViewModel.PerkCount[item.Id] Contributors</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (Model.ViewCrowdfundViewModel.Started && !Model.ViewCrowdfundViewModel.Ended)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="Email"></label>
|
||||
<input asp-for="Email" type="email" class="form-control"/>
|
||||
<span asp-validation-for="Email" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Amount"></label>
|
||||
<div class="input-group mb-3">
|
||||
<input asp-for="Amount" type="number" step="any" class="form-control"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">@Model.ViewCrowdfundViewModel.TargetCurrency.ToUpperInvariant()</span>
|
||||
</div>
|
||||
</div>
|
||||
<span asp-validation-for="Amount" class="text-danger"></span>
|
||||
</div>
|
||||
<input type="hidden" asp-for="RedirectToCheckout"/>
|
||||
|
||||
|
||||
<button type="submit" class="btn btn-primary" >Contribute</button>
|
||||
|
||||
}
|
||||
</form>
|
154
BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml
Normal file
154
BTCPayServer/Views/AppsPublic/Crowdfund/MinimalCrowdfund.cshtml
Normal file
@ -0,0 +1,154 @@
|
||||
@using BTCPayServer.Models.AppViewModels
|
||||
@using Microsoft.CodeAnalysis.CSharp.Syntax
|
||||
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
|
||||
|
||||
|
||||
<div class="container p-0">
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
|
||||
{
|
||||
<img class="card-img-top" src="@Model.MainImageUrl"/>
|
||||
}
|
||||
<div class="d-flex justify-content-between px-2">
|
||||
<h1>
|
||||
@Model.Title
|
||||
@if (!Model.Started && Model.StartDate.HasValue)
|
||||
{
|
||||
<span class="h6 text-muted" >
|
||||
Starts @Model.StartDate.Value.Subtract(DateTime.Now.ToUniversalTime())
|
||||
</span>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && Model.EndDate.HasValue)
|
||||
{
|
||||
<span class="h6 text-muted" >
|
||||
Ends @Model.EndDate.Value.Subtract(DateTime.Now.ToUniversalTime())
|
||||
</span>
|
||||
}
|
||||
else if (Model.Started && !Model.Ended && !Model.EndDate.HasValue)
|
||||
{
|
||||
<span class="h6 text-muted" :title="startDate" title="No set end date">
|
||||
Currently Active!
|
||||
</span>
|
||||
}
|
||||
|
||||
</h1>
|
||||
@if (Model.TargetAmount.HasValue)
|
||||
{
|
||||
<span class="mt-3">
|
||||
<span class="h5">@Model.TargetAmount @Model.TargetCurrency</span>
|
||||
@if (Model.ResetEveryAmount > 0 && Model.ResetEvery != nameof(CrowdfundResetEvery.Never))
|
||||
{
|
||||
<span> Dynamic</span>
|
||||
}
|
||||
@if (Model.EnforceTargetAmount)
|
||||
{
|
||||
<span >Hardcap Goal <span class="fa fa-question-circle" ></span></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span >Softcap Goal <span class="fa fa-question-circle" ></span> </span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
@if (Model.TargetAmount.HasValue)
|
||||
{
|
||||
<div class="progress w-100 rounded-0 " >
|
||||
<div class="progress-bar" role="progressbar"
|
||||
style="width:@(Model.Info.ProgressPercentage + "%")"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
<div class="progress-bar bg-warning" role="progressbar"
|
||||
style="width:@(Model.Info.PendingProgressPercentage + "%")"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
|
||||
<div class="row py-2 text-center">
|
||||
<div class="col-sm border-right" id="raised-amount">
|
||||
<h5>@(Model.Info.CurrentAmount + Model.Info.CurrentPendingAmount) @Model.TargetCurrency </h5>
|
||||
<h5 class="text-muted">Raised</h5>
|
||||
</div>
|
||||
<div class="col-sm border-right">
|
||||
<h5>@(Model.Info.PendingProgressPercentage.GetValueOrDefault(0) + Model.Info.ProgressPercentage.GetValueOrDefault(0))%</h5>
|
||||
<h5 class="text-muted">Of Goal</h5>
|
||||
</div>
|
||||
<div class="col-sm text-right">
|
||||
<h5>
|
||||
@Model.Info.TotalContributors
|
||||
</h5>
|
||||
<h5 class="text-muted">Contributors</h5>
|
||||
</div>
|
||||
@if (Model.Started && !Model.Ended)
|
||||
{
|
||||
<div class="col-sm">
|
||||
<h5>
|
||||
@TimeZoneInfo.ConvertTimeFromUtc(Model.EndDate.Value, TimeZoneInfo.Local)
|
||||
|
||||
</h5>
|
||||
<h5 class="text-muted">Ends</h5>
|
||||
</div>
|
||||
} else if (!Model.Started)
|
||||
{
|
||||
<div class="col-sm">
|
||||
<h5>
|
||||
@TimeZoneInfo.ConvertTimeFromUtc(Model.StartDate.Value, TimeZoneInfo.Local)
|
||||
</h5>
|
||||
<h5 class="text-muted">Starts</h5>
|
||||
</div>
|
||||
}else if (Model.Ended)
|
||||
{
|
||||
|
||||
<div class="col-sm" id="inactive-campaign">
|
||||
<h5>
|
||||
Campaign
|
||||
</h5>
|
||||
<h5 >not active</h5>
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-title">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 ">
|
||||
<h2 class="text-muted" >@Model.Tagline</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden">@Html.Raw(Model.Description)</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<partial
|
||||
name="Crowdfund/ContributeForm"
|
||||
model="@(new ContributeToCrowdfund()
|
||||
{
|
||||
ViewCrowdfundViewModel = Model,
|
||||
RedirectToCheckout = true,
|
||||
|
||||
})"></partial>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-muted d-flex" >
|
||||
|
||||
<div class="align-self-end pr-4">Updated @Model.Info.LastUpdated</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
306
BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml
Normal file
306
BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml
Normal file
@ -0,0 +1,306 @@
|
||||
<div class="container p-0" id="app" v-cloak>
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl">
|
||||
<div class="d-flex justify-content-between px-2">
|
||||
<h1>
|
||||
{{srvModel.title}}
|
||||
<span class="h6 text-muted" v-if="!started && srvModel.startDate" v-b-tooltip :title="startDate">
|
||||
Starts {{startDateRelativeTime}}
|
||||
</span>
|
||||
<span class="h6 text-muted" v-else-if="started && !ended && srvModel.endDate" v-b-tooltip :title="endDate">
|
||||
Ends {{endDateRelativeTime}}
|
||||
</span>
|
||||
<span class="h6 text-muted" v-else-if="started && !ended && !srvModel.endDate" v-b-tooltip title="No set end date">
|
||||
Currently Active!
|
||||
</span>
|
||||
|
||||
</h1>
|
||||
|
||||
<span v-if="srvModel.targetAmount" class="mt-3">
|
||||
<span class="h5">{{srvModel.targetAmount}} {{targetCurrency}}</span>
|
||||
<span v-if="srvModel.resetEvery !== 'Never'" v-b-tooltip
|
||||
:title="'Goal resets every ' + srvModel.resetEveryAmount + ' ' + srvModel.resetEvery + ((srvModel.resetEveryAmount>1)?'s': '')" >Dynamic </span>
|
||||
<span v-if="srvModel.enforceTargetAmount">Hardcap Goal <span class="fa fa-question-circle" v-b-tooltip title="No contributions allowed after the goal has been reached"></span></span>
|
||||
<span v-else>Softcap Goal <span class="fa fa-question-circle" v-b-tooltip title="Contributions allowed even after goal is reached"></span> </span>
|
||||
</span>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="progress w-100 rounded-0 " v-if="srvModel.targetAmount">
|
||||
<div class="progress-bar" role="progressbar"
|
||||
:aria-valuenow="srvModel.info.progressPercentage"
|
||||
v-bind:style="{ width: srvModel.info.progressPercentage + '%' }"
|
||||
aria-valuemin="0"
|
||||
v-b-tooltip :title="parseFloat(srvModel.info.progressPercentage).toFixed(2) + '% confirmed 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) + '% pending contributions'"
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
|
||||
<div class="row py-2 text-center">
|
||||
<div class="col-sm border-right" id="raised-amount">
|
||||
<h5>{{ raisedAmount }} {{targetCurrency}} </h5>
|
||||
<h5 class="text-muted">Raised</h5>
|
||||
</div>
|
||||
<div class="col-sm border-right" id="goal-raised">
|
||||
<h5>{{ percentageRaisedAmount }}%</h5>
|
||||
<h5 class="text-muted">Of Goal</h5>
|
||||
</div>
|
||||
<div class="col-sm border-right">
|
||||
<h5>
|
||||
{{srvModel.info.totalContributors}}
|
||||
</h5>
|
||||
<h5 class="text-muted">Contributors</h5>
|
||||
</div>
|
||||
<div class="col-sm" v-if="endDiff" id="campaign-dates-started">
|
||||
<h5>
|
||||
{{endDiff}}
|
||||
</h5>
|
||||
<h5 class="text-muted">Left</h5>
|
||||
<b-tooltip target="campaign-dates-started" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
</li>
|
||||
<li v-if="endDate" class="list-unstyled">
|
||||
{{ended? "Ended" : "Ends"}} {{endDate}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="col-sm" v-if="startDiff" id="campaign-dates-not-started">
|
||||
<h5>
|
||||
{{startDiff}}
|
||||
</h5>
|
||||
<h5 class="text-muted">Left to start</h5>
|
||||
|
||||
<b-tooltip target="campaign-dates-ended" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
</li>
|
||||
<li v-if="endDate" class="list-unstyled">
|
||||
{{ended? "Ended" : "Ends"}} {{endDate}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
<div class="col-sm" v-if="ended" id="campaign-dates-ended">
|
||||
<h5>
|
||||
Campaign
|
||||
</h5>
|
||||
<h5 >not active</h5>
|
||||
|
||||
<b-tooltip target="campaign-dates-not-started" >
|
||||
<ul class="p-0">
|
||||
<li v-if="startDate" class="list-unstyled">
|
||||
{{started? "Started" : "Starts"}} {{startDate}}
|
||||
</li>
|
||||
<li v-if="endDate" class="list-unstyled">
|
||||
{{ended? "Ended" : "Ends"}} {{endDate}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<b-tooltip target="raised-amount" v-if="paymentStats && paymentStats.length > 0">
|
||||
<ul class="p-0 text-uppercase">
|
||||
<li v-for="stat of paymentStats" class="list-unstyled">
|
||||
|
||||
{{stat.label}} <span v-if="stat.lightning" class="fa fa-bolt"></span> {{stat.value}}
|
||||
</li>
|
||||
</ul>
|
||||
</b-tooltip>
|
||||
<b-tooltip target="goal-raised" v-if="srvModel.resetEvery !== 'Never'">
|
||||
Goal resets every {{srvModel.resetEveryAmount}} {{srvModel.resetEvery}} {{srvModel.resetEveryAmount>1?'s': ''}}
|
||||
</b-tooltip>
|
||||
|
||||
|
||||
<div class="card-title">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-8 col-lg-9">
|
||||
|
||||
|
||||
<h2 class="text-muted" v-if="srvModel.tagline">{{srvModel.tagline}}</h2>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4 col-lg-3">
|
||||
<button v-if="active" class="btn btn-lg btn-primary w-100 font-weight-bold" v-on:click="contributeModalOpen = true">Contribute</button>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="srvModel.disqusEnabled">
|
||||
<b-tabs>
|
||||
<b-tab title="Details"active>
|
||||
<div class="row ">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description"></div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
:active="active"
|
||||
:in-modal="false"
|
||||
:perks="perks">
|
||||
|
||||
</contribute>
|
||||
</div>
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab title="Discussion" >
|
||||
<div id="disqus_thread" class=" mt-2"></div>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
|
||||
</template>
|
||||
<template v-else>
|
||||
<hr/>
|
||||
<div class="row mt-2">
|
||||
<div class="col-md-8 col-sm-12">
|
||||
<div class="card-text overflow-hidden" v-html="srvModel.description"></div>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-12">
|
||||
<contribute :target-currency="srvModel.targetCurrency"
|
||||
:display-perks-ranking="srvModel.displayPerksRanking"
|
||||
:active="active"
|
||||
:in-modal="false"
|
||||
:perks="perks">
|
||||
|
||||
</contribute>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="card-footer text-muted d-flex" v-if="srvModel.animationsEnabled || srvModel.soundsEnabled">
|
||||
|
||||
<div class="align-self-end pr-4">Updated {{lastUpdated}}</div>
|
||||
<div class="form-check mx-1" v-if="srvModel.animationsEnabled || animation">
|
||||
<input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation">
|
||||
<label class="form-check-label" for="cbAnime">
|
||||
Animations
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mx-1" v-if="srvModel.soundsEnabled|| sound">
|
||||
<input class="form-check-input" type="checkbox" id="cbSounds" v-model="sound">
|
||||
<label class="form-check-label" for="cbSounds">
|
||||
Sounds
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="Close" ref="modalContribute">
|
||||
|
||||
<contribute v-if="contributeModalOpen"
|
||||
:target-currency="srvModel.targetCurrency"
|
||||
:active="active"
|
||||
:perks="srvModel.perks"
|
||||
:in-modal="true">
|
||||
</contribute>
|
||||
</b-modal>
|
||||
|
||||
</div>
|
||||
|
||||
<script type="text/x-template" id="perks-template">
|
||||
<div>
|
||||
<perk v-if="!perks || perks.length ===0" :perk="{title: 'Donate Custom Amount', price: { value: null}, custom: true}" :target-currency="targetCurrency" :active="active"
|
||||
:in-modal="inModal">
|
||||
</perk>
|
||||
<perk v-for="(perk, index) in perks" :perk="perk" :target-currency="targetCurrency" :active="active" :display-perks-ranking="displayPerksRanking" :index="index"
|
||||
:in-modal="inModal">
|
||||
</perk>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-template" id="perk-template">
|
||||
<div class="card mb-4 perk" v-bind:class="{ 'expanded': expanded, 'unexpanded': !expanded }" v-on:click="expand" :id="perk.id">
|
||||
<span v-if="displayPerksRanking && perk.sold"
|
||||
class="btn btn-sm rounded-circle px-0 perk-badge"
|
||||
v-bind:class="{ 'btn-primary': index==0, 'btn-secondary': index!=0}">#{{index+1}}</span>
|
||||
|
||||
|
||||
<div class="perk-zoom " v-if="canExpand">
|
||||
<div class="perk-zoom-bg bg-primary"> </div>
|
||||
<div class="perk-zoom-text w-100 text-center text-white font-weight-bold">
|
||||
Select this contribution perk
|
||||
</div>
|
||||
</div>
|
||||
<form v-on:submit='onContributeFormSubmit'>
|
||||
<input type="hidden" :value="perk.id" id="choiceKey"/>
|
||||
<img v-if="perk.image && perk.image != 'null' " class="card-img-top" :src="perk.image" />
|
||||
<div class="card-body">
|
||||
<div class="card-title d-flex justify-content-between" >
|
||||
<span class="h5">{{perk.title? perk.title : perk.id}} </span>
|
||||
<span class="text-muted" >
|
||||
<template v-if="perk.price.value">{{perk.price.value.noExponents()}}
|
||||
{{targetCurrency}}
|
||||
<template v-if="perk.custom">or more</template>
|
||||
</template>
|
||||
<template v-else-if="!perk.price.value && perk.custom">
|
||||
Any amount
|
||||
</template>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
<p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
|
||||
|
||||
<div class="input-group" style="max-width: 500px;" v-if="expanded" :id="'perk-form'+ perk.id">
|
||||
<input
|
||||
:disabled="!active"
|
||||
:readonly="!perk.custom"
|
||||
class="form-control"
|
||||
type="number"
|
||||
v-model="amount"
|
||||
:min="perk.price.value"
|
||||
step="any"
|
||||
placeholder="Contribution Amount"
|
||||
required >
|
||||
<div class="input-group-append">
|
||||
<span class='input-group-text'>{{targetCurrency}}</span>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
:disabled="!active"
|
||||
type="submit">
|
||||
Continue
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer d-flex justify-content-between" v-if="perk.sold">
|
||||
<span ></span>
|
||||
<span x >{{perk.sold}} Contributor{{perk.sold > 1? "s": ""}}</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/x-template" id="contribute-template">
|
||||
<div>
|
||||
<h3 v-if="!inModal" class="mb-3">Contribute</h3>
|
||||
<perks
|
||||
:perks="perks"
|
||||
:in-modal="inModal"
|
||||
:display-perks-ranking="displayPerksRanking"
|
||||
:target-currency="targetCurrency"
|
||||
:active="active">
|
||||
</perks>
|
||||
</div>
|
||||
</script>
|
58
BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml
Normal file
58
BTCPayServer/Views/AppsPublic/ViewCrowdfund.cshtml
Normal file
@ -0,0 +1,58 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
|
||||
@{
|
||||
ViewData["Title"] = Model.Title;
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html class="h-100">
|
||||
<head>
|
||||
<title>@Model.Title</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link href="@Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
}
|
||||
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-1.min.js"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle-2.min.js"></bundle>
|
||||
}
|
||||
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
@if (Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
|
||||
}
|
||||
else
|
||||
{
|
||||
<noscript>
|
||||
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
|
||||
</noscript>
|
||||
|
||||
if (Model.AnimationsEnabled)
|
||||
{
|
||||
<canvas id="fireworks"></canvas>
|
||||
}
|
||||
@await Html.PartialAsync("Crowdfund/VueCrowdfund", Model)
|
||||
}
|
||||
|
||||
</body>
|
||||
</html>
|
@ -5,6 +5,7 @@
|
||||
@{
|
||||
ViewData["Title"] = Model.Title;
|
||||
Layout = null;
|
||||
int[] CustomTipPercentages = Model.CustomTipPercentages;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
@ -14,6 +15,11 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
|
||||
<link rel="apple-touch-startup-image" href="~/img/splash.png">
|
||||
|
||||
<link rel="manifest" href="~/manifest.json">
|
||||
|
||||
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
|
||||
@if (Model.CustomCSSLink != null)
|
||||
{
|
||||
@ -23,125 +29,330 @@
|
||||
|
||||
@if (Model.EnableShoppingCart)
|
||||
{
|
||||
<link rel="stylesheet" href="~/cart/css/style.css">
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
|
||||
}
|
||||
</head>
|
||||
|
||||
<script id="template-cart-item" type="text/template">
|
||||
<tr data-id="{id}">
|
||||
{image}
|
||||
<td class="align-middle pr-0 pl-2"><b>{title}</b></td>
|
||||
<td class="align-middle px-0" align="right">
|
||||
<a class="js-cart-item-remove btn btn-link" href="#"><i class="fa fa-trash text-muted"></i></a>
|
||||
</td>
|
||||
<td class="align-middle px-0" align="right">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
|
||||
</div>
|
||||
<input class="js-cart-item-count form-control form-control-sm pull-left" type="text"name="count" placeholder="Qty" value="{count}" data-prev="{count}">
|
||||
<div class="input-group-append"><a class="js-cart-item-plus btn btn-link px-2" href="#">
|
||||
<i class="fa fa-plus-circle fa-fw text-success"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle" align="right">{price}</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<script id="template-cart-item-image" type="text/template">
|
||||
<td class="align-middle pr-0" width="1%"><img src="{image}" width="50"></td>
|
||||
</script>
|
||||
|
||||
<script id="template-cart-custom-amount" type="text/template">
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
|
||||
</div>
|
||||
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Pay what you want">
|
||||
<div class="input-group-append">
|
||||
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<script id="template-cart-extra" type="text/template">
|
||||
<tr>
|
||||
<td colspan="5" class="border-0 pb-0">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
|
||||
</div>
|
||||
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
|
||||
<div class="input-group-append">
|
||||
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5" class="border-top-0">
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
|
||||
</div>
|
||||
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
|
||||
<div class="input-group-append">
|
||||
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<script id="template-cart-tip" type="text/template">
|
||||
<tr class="h5">
|
||||
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="5" class="border-0">
|
||||
<div class="input-group mb-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
|
||||
</div>
|
||||
<input class="js-cart-tip form-control" type="number" min="0" step="@Model.Step" value="{tip}" name="tip" placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol != null ? Model.CurrencyInfo.CurrencySymbol : Model.CurrencyCode)">
|
||||
<div class="input-group-append">
|
||||
<a class="js-cart-tip-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-1">
|
||||
@if (CustomTipPercentages != null && CustomTipPercentages.Length > 0) {
|
||||
@for (int i = 0; i < CustomTipPercentages.Length; i++) {
|
||||
var percentage = CustomTipPercentages[i];
|
||||
<div class="col">
|
||||
<a class="js-cart-tip-btn btn btn-light btn-block border mb-2" href="#" data-tip="@percentage">@percentage%</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<script id="template-cart-total" type="text/template">
|
||||
<tr class="h4 table-light">
|
||||
<td colspan="1" class="pb-4">Total</td>
|
||||
<td colspan="4" align="right" class="pb-4">
|
||||
<span class="js-cart-total">{total}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<body class="h-100">
|
||||
@if (Model.EnableShoppingCart)
|
||||
{
|
||||
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Shopping cart</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-primary text-white border-0">
|
||||
<h5 class="modal-title">Confirmation</h5>
|
||||
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true"><i class="fa fa-times fa-fw"></i></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<table id="js-cart-summary" class="table m-0">
|
||||
<tbody class="my-3">
|
||||
<tr class="h5">
|
||||
<td colspan="2" class="border-top-0">Summary</td>
|
||||
</tr>
|
||||
<tr class="h6">
|
||||
<td class="border-0 pb-0">Total products</td>
|
||||
<td align="right" class="border-0 pb-0">
|
||||
<span class="js-cart-summary-products text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h6">
|
||||
<td class="border-0 pb-y">Discount</td>
|
||||
<td align="right" class="border-0 pb-y">
|
||||
<span class="js-cart-summary-discount text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h6">
|
||||
<td class="border-top-0 pt-0">Tip</td>
|
||||
<td align="right" class="border-top-0 pt-0">
|
||||
<span class="js-cart-summary-tip text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="h3 table-light">
|
||||
<td>Total</td>
|
||||
<td align="right">
|
||||
<span class="js-cart-summary-total text-nowrap"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer bg-light">
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
|
||||
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<table id="js-cart-list" class="table mt-2 mb-3">
|
||||
</div>
|
||||
|
||||
<div class="wrapper">
|
||||
<!-- Page Content -->
|
||||
<div id="content">
|
||||
<div class="p-3">
|
||||
<div class="row">
|
||||
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
|
||||
<a class="js-cart btn btn-warning text-white text-right" href="#"><i class="fa fa-shopping-basket"></i> <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
|
||||
</div>
|
||||
<div class="col-sm-8 col-lg-9 mb-2">
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="js-search form-control" placeholder="Find product">
|
||||
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute;right: 0px; z-index: 1049; display: none;"><i class="fa fa-times-circle fa-lg"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="js-pos-list" class="text-center mx-auto px-4">
|
||||
<div class="row">
|
||||
@for (int i = 0; i < Model.Items.Length; i++)
|
||||
{
|
||||
var item = Model.Items[i];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
|
||||
<div class="col-sm-6 col-lg-3 my-3 px-2 card-wrapper">
|
||||
<div class="js-add-cart card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body p-3">
|
||||
<h6 class="card-title mb-0">@item.Title</h6>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav id="sidebar" class="bg-dark">
|
||||
<div class="bg-warning p-3 clearfix">
|
||||
<h3 class="text-white m-0 pull-left">Cart</h3>
|
||||
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#"><i class="fa fa-times fa-lg"></i></a>
|
||||
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
|
||||
</div>
|
||||
|
||||
<table id="js-cart-list" class="table table-responsive bg-light mt-0 mb-0">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th colspan="2">Product</th>
|
||||
<th class="text-right" width="80">Quantity</th>
|
||||
<th class="text-right" width="25%">Price</th>
|
||||
<th colspan="3" width="55%">Product</th>
|
||||
<th class="text-center" width="20%"><div style="width: 84px">Quantity</div></th>
|
||||
<th class="text-right" width="25%"><div style="min-width: 50px">Price</div></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
|
||||
<button id="js-cart-pay" class="btn btn-primary" type="submit"><b>@Model.CustomButtonText</b></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="container d-flex h-100">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
@if (Model.EnableShoppingCart)
|
||||
{
|
||||
<a id="js-cart" class="btn btn-warning text-white text-right" href="#" data-toggle="modal" data-target="#cartModal"><i class="fa fa-shopping-basket"></i> <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
|
||||
}
|
||||
<div class="row">
|
||||
@for (int i = 0; i < Model.Items.Length; i++)
|
||||
{
|
||||
var className = (Model.Items.Length - i) > (Model.Items.Length % 4) ? "col-sm-6 col-lg-3" : "col-md align-self-start";
|
||||
var item = Model.Items[i];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
<div class="@className my-3 px-2">
|
||||
<div class="card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@item.Title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit"><b>Confirm</b></button>
|
||||
|
||||
<div class="text-center mb-5 pb-5">
|
||||
<img src="~/img/logo-white.png" height="40">
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
} else {
|
||||
<div class="container d-flex h-100">
|
||||
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
|
||||
<h1 class="mb-4">@Model.Title</h1>
|
||||
<div class="row">
|
||||
@for (int i = 0; i < Model.Items.Length; i++)
|
||||
{
|
||||
var className = (Model.Items.Length - i) > (Model.Items.Length % 4) ? "col-sm-6 col-lg-3" : "col-md align-self-start";
|
||||
var item = Model.Items[i];
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
<div class="@className my-3 px-2">
|
||||
<div class="card" data-id="@i">
|
||||
@if (!String.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||
}
|
||||
@if (item.Custom && !Model.EnableShoppingCart)
|
||||
{
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@item.Title</h5>
|
||||
@if (!String.IsNullOrWhiteSpace(description))
|
||||
{
|
||||
<p class="card-text">@description</p>
|
||||
}
|
||||
@if (item.Custom && !Model.EnableShoppingCart)
|
||||
{
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input type="hidden" name="choicekey" value="@item.Id" />
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
|
||||
value="@item.Price.Value" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-antiforgery="false">
|
||||
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="row mt-2 mb-4">
|
||||
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Custom Amount</h5>
|
||||
<p class="card-text">Create invoice to pay custom amount</p>
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<input type="hidden" name="choicekey" value="@item.Id" />
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
|
||||
value="@item.Price.Value" placeholder="Amount">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
|
||||
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
else
|
||||
{
|
||||
<form method="post" asp-antiforgery="false">
|
||||
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (Model.ShowCustomAmount)
|
||||
{
|
||||
<div class="row mt-2 mb-4">
|
||||
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Custom Amount</h5>
|
||||
<p class="card-text">Create invoice to pay custom amount</p>
|
||||
<form method="post" asp-antiforgery="false" data-buy>
|
||||
<div class="input-group">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">@Model.CurrencySymbol</span>
|
||||
</div>
|
||||
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
|
||||
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -151,7 +151,7 @@
|
||||
<div class="payment-tabs__tab" id="copy-tab">
|
||||
<span>{{$t("Copy")}}</span>
|
||||
</div>
|
||||
@if (Model.ChangellyEnabled)
|
||||
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
|
||||
{
|
||||
<div class="payment-tabs__tab" id="altcoins-tab">
|
||||
<span>{{$t("Conversion")}}</span>
|
||||
@ -256,7 +256,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
@if (Model.ChangellyEnabled)
|
||||
@if (Model.ChangellyEnabled || Model.CoinSwitchEnabled)
|
||||
{
|
||||
<div id="altcoins" class="bp-view payment manual-flow">
|
||||
<nav v-if="srvModel.isLightning">
|
||||
@ -274,42 +274,99 @@
|
||||
{{$t("ConversionTab_BodyDesc", srvModel)}}
|
||||
</span>
|
||||
</div>
|
||||
<center>
|
||||
<changelly inline-template
|
||||
:merchant-id="srvModel.changellyMerchantId"
|
||||
:store-id="srvModel.storeId"
|
||||
:to-currency="srvModel.paymentMethodId"
|
||||
:to-currency-due="srvModel.changellyAmountDue"
|
||||
:to-currency-address="srvModel.btcAddress">
|
||||
<div class="changelly-component">
|
||||
<div class="changelly-component-dropdown-holder" v-show="prettyDropdownInstance">
|
||||
<select
|
||||
<center>
|
||||
|
||||
@if (Model.CoinSwitchEnabled && Model.ChangellyEnabled)
|
||||
{
|
||||
<template v-if="!selectedThirdPartyProcessor">
|
||||
<button v-on:click="selectedThirdPartyProcessor = 'coinswitch'" class="action-button">
|
||||
{{$t("Pay with CoinSwitch")}}
|
||||
</button>
|
||||
<button v-on:click="selectedThirdPartyProcessor = 'changelly'" class="action-button">
|
||||
{{$t("Pay with Changelly")}}
|
||||
</button>
|
||||
</template>
|
||||
}
|
||||
|
||||
@if (Model.CoinSwitchEnabled)
|
||||
{
|
||||
<coinswitch inline-template
|
||||
v-if="!srvModel.changellyEnabled || selectedThirdPartyProcessor === 'coinswitch'"
|
||||
:mode="srvModel.coinSwitchMode"
|
||||
:merchant-id="srvModel.coinSwitchMerchantId"
|
||||
:to-currency="srvModel.paymentMethodId"
|
||||
:to-currency-due="srvModel.btcDue"
|
||||
:autoload="selectedThirdPartyProcessor === 'coinswitch'"
|
||||
:to-currency-address="srvModel.btcAddress">
|
||||
<div>
|
||||
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url && !opened">
|
||||
{{$t("Pay with CoinSwitch")}}
|
||||
</a>
|
||||
|
||||
@if (Model.ChangellyEnabled)
|
||||
{
|
||||
<button v-show="!opened" v-on:click="$parent.selectedThirdPartyProcessor = 'changelly'" class="btn-link mt-2">
|
||||
{{$t("Pay with Changelly")}}
|
||||
</button>
|
||||
}
|
||||
|
||||
<iframe
|
||||
v-if="showInlineIFrame"
|
||||
v-on:load="onLoadIframe"
|
||||
style="height: 100%; position: fixed; top: 0; width: 100%; left: 0;"
|
||||
sandbox="allow-scripts allow-forms allow-popups allow-same-origin"
|
||||
:src="url"></iframe>
|
||||
|
||||
</div>
|
||||
|
||||
</coinswitch>
|
||||
}
|
||||
|
||||
@if(Model.ChangellyEnabled){
|
||||
|
||||
<changelly inline-template
|
||||
v-if="!srvModel.coinSwitchEnabled || selectedThirdPartyProcessor === 'changelly'"
|
||||
:merchant-id="srvModel.changellyMerchantId"
|
||||
:store-id="srvModel.storeId"
|
||||
:to-currency="srvModel.paymentMethodId"
|
||||
:to-currency-due="srvModel.changellyAmountDue"
|
||||
:to-currency-address="srvModel.btcAddress">
|
||||
<div class="changelly-component">
|
||||
<div class="changelly-component-dropdown-holder" v-show="prettyDropdownInstance">
|
||||
<select
|
||||
v-model="selectedFromCurrency"
|
||||
:disabled="isLoading"
|
||||
v-on:change="onCurrencyChange($event)"
|
||||
ref="changellyCurrenciesDropdown">
|
||||
<option value="">{{$t("ConversionTab_CurrencyList_Select_Option")}}</option>
|
||||
<option v-for="currency of currencies"
|
||||
:data-prefix="'<img src=\''+currency.image+'\'/>'"
|
||||
:value="currency.name">
|
||||
{{currency.fullName}}
|
||||
</option>
|
||||
</select>
|
||||
<option value="">{{$t("ConversionTab_CurrencyList_Select_Option")}}</option>
|
||||
<option v-for="currency of currencies"
|
||||
:data-prefix="'<img src=\''+currency.image+'\'/>'"
|
||||
:value="currency.name">
|
||||
{{currency.fullName}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<a v-on:click="openDialog($event)" :href="url" class="action-button" v-show="url">
|
||||
{{$t("Pay with Changelly")}}
|
||||
</a>
|
||||
@if (Model.CoinSwitchEnabled)
|
||||
{
|
||||
<button v-on:click="$parent.selectedThirdPartyProcessor = 'coinswitch'" class="btn-link mt-2">
|
||||
{{$t("Pay with CoinSwitch")}}
|
||||
</button>
|
||||
}
|
||||
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
|
||||
{{$t("ConversionTab_CalculateAmount_Error")}}
|
||||
</button>
|
||||
<button class="retry-button" v-if="currenciesError" v-on:click="retry('loadCurrencies')">
|
||||
{{$t("ConversionTab_LoadCurrencies_Error")}}
|
||||
</button>
|
||||
<div v-show="isLoading" class="general__spinner">
|
||||
<partial name="Checkout-Spinner"/>
|
||||
</div>
|
||||
</div>
|
||||
<a v-on:click="openDialog($event)" :href="url" class="btn btn-primary retry-button changelly-component-button" v-show="url">
|
||||
Pay with Changelly
|
||||
</a>
|
||||
<button class="retry-button" v-if="calculateError" v-on:click="retry('calculateAmount')">
|
||||
{{$t("ConversionTab_CalculateAmount_Error")}}
|
||||
</button>
|
||||
<button class="retry-button" v-if="currenciesError" v-on:click="retry('loadCurrencies')">
|
||||
{{$t("ConversionTab_LoadCurrencies_Error")}}
|
||||
</button>
|
||||
<div v-show="isLoading" class="general__spinner">
|
||||
<partial name="Checkout-Spinner"/>
|
||||
</div>
|
||||
</div>
|
||||
</changelly>
|
||||
</changelly>
|
||||
}
|
||||
</center>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -170,14 +170,16 @@
|
||||
el: '#checkoutCtrl',
|
||||
components: {
|
||||
qrcode: VueQr,
|
||||
changelly: ChangellyComponent
|
||||
changelly: ChangellyComponent,
|
||||
coinswitch: CoinSwitchComponent
|
||||
},
|
||||
data: {
|
||||
srvModel: srvModel,
|
||||
lndModel: null,
|
||||
scanDisplayQr: "",
|
||||
expiringSoon: false,
|
||||
isModal: srvModel.isModal
|
||||
isModal: srvModel.isModal,
|
||||
selectedThirdPartyProcessor: ""
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -29,6 +29,8 @@
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>storeid:id</code> for filtering a specific store</li>
|
||||
<li><code>orderid:id</code> for filtering a specific order</li>
|
||||
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
|
||||
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
|
||||
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
|
||||
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>
|
||||
@ -47,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>
|
||||
|
@ -0,0 +1,140 @@
|
||||
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||
|
||||
@model BTCPayServer.Controllers.ShowLightningNodeInfoViewModel
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>@Model.CryptoCode LN Node Info</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 rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
|
||||
<link rel="apple-touch-startup-image" href="~/img/splash.png">
|
||||
|
||||
<link rel="manifest" href="~/manifest.json">
|
||||
|
||||
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet"/>
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
|
||||
|
||||
<bundle name="wwwroot/bundles/lightning-node-info-bundle.min.js"/>
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
|
||||
|
||||
window.onload = function() {
|
||||
Vue.use(Toasted);
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
components: {
|
||||
qrcode: VueQr
|
||||
},
|
||||
data: {
|
||||
srvModel: srvModel
|
||||
},
|
||||
mounted: function() {
|
||||
|
||||
this.$nextTick(function() {
|
||||
var copyInput = new Clipboard('.copy');
|
||||
|
||||
copyInput.on("success",
|
||||
function(e) {
|
||||
Vue.toasted.show('Copied',
|
||||
{
|
||||
iconPack: "fontawesome",
|
||||
icon: "copy",
|
||||
duration: 5000
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.qr-icon {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
.copy {
|
||||
cursor: copy;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body >
|
||||
<noscript>
|
||||
<div class="container">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
@Model.CryptoCode Lightning Node - @(Model.Available? "Online" : "Unavailable")
|
||||
<small class="@(Model.Available? "text-success" : "text-danger")">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
@if (Model.Available)
|
||||
{
|
||||
<div class="card-body m-sm-0 p-sm-0" >
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control " readonly="readonly" asp-for="NodeInfo" id="peer-info"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
<div id="app" class="container">
|
||||
<div class="row " style="height: 100vh">
|
||||
<div class="col-md-8 col-sm-12 col-lg-6 mx-auto my-auto ">
|
||||
<div class="card border-0">
|
||||
<div class="row"></div>
|
||||
<h1 class="card-title text-center">
|
||||
{{srvModel.cryptoCode}} Lightning Node
|
||||
- {{srvModel.available? "Online" : "Unavailable"}}
|
||||
<small v-bind:class="{ 'text-success': srvModel.available, 'text-danger': !srvModel.available }">
|
||||
<span class="fa fa-circle "></span>
|
||||
</small>
|
||||
</h1>
|
||||
<div class="card-body m-sm-0 p-sm-0" v-if="srvModel.available">
|
||||
<div class="qr-container mb-2">
|
||||
<img v-bind:src="srvModel.cryptoImage" class="qr-icon"/>
|
||||
<qrcode v-bind:val="srvModel.nodeInfo" v-bind:size="256" bg-color="#f5f5f7" fg-color="#000">
|
||||
</qrcode>
|
||||
</div>
|
||||
<div class="input-group copy" data-clipboard-target="#vue-peer-info">
|
||||
<input type="text" class=" form-control " readonly="readonly" :value="srvModel.nodeInfo" id="vue-peer-info"/>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text fa fa-copy"> </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -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>
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
<span>The DerivationScheme represents the destination of the funds received by your invoice. It is generated by your wallet software. Please, verify that you are generating the right addresses by clicking on 'Check ExtPubKey'</span>
|
||||
</div>
|
||||
<input id="CryptoCurrency" asp-for="CryptoCode" type="hidden" />
|
||||
<input id="KeyPath" asp-for="KeyPath" type="hidden" />
|
||||
<div class="form-group">
|
||||
<label asp-for="DerivationScheme"></label>
|
||||
<input asp-for="DerivationScheme" class="form-control" />
|
||||
@ -36,11 +37,11 @@
|
||||
No ledger wallet detected. If you own one, use chrome, open the app, and refresh this page.
|
||||
</p>
|
||||
<div id="ledger-info" class="form-text text-muted" style="display: none;">
|
||||
<span>A ledger wallet is detected, which account do you want to use?</span>
|
||||
<span>A ledger wallet is detected, which account do you want to use? No need to paste manually xpub if your ledger device was detected. Just select derivation scheme from the list bellow and xpub will automatically populate.</span>
|
||||
<ul>
|
||||
@for (int i = 0; i < 4; i++)
|
||||
{
|
||||
<li><a class="ledger-info-recommended" data-ledgeraccount="@i" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
|
||||
<li><a class="ledger-info-recommended" data-ledgerkeypath="@Model.RootKeyPath.Derive(i, true)" href="#">Account @i (<span>@Model.RootKeyPath.Derive(i, true)</span>)</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@ -95,6 +96,7 @@
|
||||
<span>Please check that your @Model.CryptoCode wallet is generating the same addresses as below.</span>
|
||||
</div>
|
||||
<input type="hidden" asp-for="Confirmation" />
|
||||
<input id="KeyPath" asp-for="KeyPath" type="hidden" />
|
||||
<input type="hidden" asp-for="DerivationScheme" />
|
||||
<input type="hidden" asp-for="Enabled" />
|
||||
<div class="form-group">
|
||||
|
@ -85,6 +85,15 @@
|
||||
</div>
|
||||
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
|
||||
<button name="command" type="submit" value="test" class="btn btn-secondary">Test connection</button>
|
||||
<a
|
||||
class="btn btn-secondary"
|
||||
asp-controller="PublicLightningNodeInfo"
|
||||
asp-action="ShowLightningNodeInfo"
|
||||
asp-route-cryptoCode="@Model.CryptoCode"
|
||||
asp-route-storeId="@Model.StoreId"
|
||||
target="_blank">
|
||||
Open Public Node Info Page
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,15 +43,15 @@
|
||||
<div class="form-group">
|
||||
<label>Button Size</label>
|
||||
<div style="vertical-align:top; font-size:12px; display:flex;">
|
||||
<button class="btn" style="width:95px;height:40px;margin-right:40px;"
|
||||
<button class="btn" style="width:146px;height:40px;margin-right:40px;"
|
||||
v-on:click="inputChanges($event, 0)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 0) }">
|
||||
146 x 40 px
|
||||
</button>
|
||||
<button class="btn btn-default" style="width:126px;height:46px;margin-right:40px;"
|
||||
<button class="btn btn-default" style="width:168px;height:46px;margin-right:40px;"
|
||||
v-on:click="inputChanges($event, 1)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 1) }">
|
||||
168 x 46 px
|
||||
</button>
|
||||
<button class="btn btn-default" style="width:146px;height:57px;"
|
||||
<button class="btn btn-default" style="width:209px;height:57px;"
|
||||
v-on:click="inputChanges($event, 2)" v-bind:class="{'btn-primary': (srvModel.buttonSize == 2) }">
|
||||
209 x 57 px
|
||||
</button>
|
||||
|
@ -1,6 +1,6 @@
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Please confirm you want to allow anyone to create invoices in your store");
|
||||
ViewData.SetActivePageAndTitle(StoreNavPages.PayButton, "Please confirm you want to allow outside world to create invoices in your store (via API).");
|
||||
ViewBag.MainTitle = "Pay Button";
|
||||
}
|
||||
|
||||
@ -12,10 +12,10 @@
|
||||
<div class="form-group">
|
||||
<p>
|
||||
To start using Pay Buttons you need to explicitly turn on this feature.
|
||||
Once you do so, any source will be able to create an invoice on your instance store.
|
||||
Once you do so, any 3rd party entity will be able to create an invoice on your instance store (via API for example).
|
||||
</p>
|
||||
@Html.Hidden("EnableStore", true)
|
||||
<button name="command" type="submit" value="save" class="btn btn-primary">Allow anyone to create invoices</button>
|
||||
<button name="command" type="submit" value="save" class="btn btn-primary">Allow outside world to create invoices via API (POS app is preauthorised and does not need this enabled).</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
42
BTCPayServer/Views/Stores/UpdateCoinSwitchSettings.cshtml
Normal file
42
BTCPayServer/Views/Stores/UpdateCoinSwitchSettings.cshtml
Normal file
@ -0,0 +1,42 @@
|
||||
@using Microsoft.AspNetCore.Mvc.Rendering
|
||||
@model UpdateCoinSwitchSettingsViewModel
|
||||
@{
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePageAndTitle(StoreNavPages.Index, "Update Store CoinSwitch Settings");
|
||||
}
|
||||
|
||||
<h4>@ViewData["Title"]</h4>
|
||||
<partial name="_StatusMessage" for="StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<form method="post">
|
||||
<p>
|
||||
You can obtain a merchant id at
|
||||
<a href="https://coinswitch.co/switch/setup/btcpay" target="_blank">
|
||||
https://coinswitch.co/switch/setup/btcpay
|
||||
</a>
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label asp-for="MerchantId"></label>
|
||||
<input asp-for="MerchantId" class="form-control"/>
|
||||
<span asp-validation-for="MerchantId" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Mode"></label>
|
||||
<select asp-for="Mode" asp-items="Model.Modes" class="form-control" >
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="Enabled"></label>
|
||||
<input asp-for="Enabled" type="checkbox" class="form-check"/>
|
||||
</div>
|
||||
<button name="command" type="submit" value="save" class="btn btn-primary">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@await Html.PartialAsync("_ValidationScriptsPartial")
|
||||
}
|
@ -43,9 +43,13 @@
|
||||
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="NetworkFee"></label>
|
||||
<label asp-for="NetworkFeeMode"></label>
|
||||
<a href="https://docs.btcpayserver.org/faq-and-common-issues/faq-stores#add-network-fee-to-invoice-vary-with-mining-fees" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||
<input asp-for="NetworkFee" type="checkbox" class="form-check" />
|
||||
<select asp-for="NetworkFeeMode" class="form-control">
|
||||
<option value="MultiplePaymentsOnly">... only if the customer makes more than one payment for the invoice</option>
|
||||
<option value="Always">... on every payment</option>
|
||||
<option value="Never">Never add network fee</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="AnyoneCanCreateInvoice"></label>
|
||||
|
@ -28,10 +28,6 @@
|
||||
<p id="hw-loading"><span class="fa fa-question-circle" style="color:orange"></span> <span>Detecting hardware wallet...</span></p>
|
||||
<p id="hw-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="hw-label">An error happened</span></p>
|
||||
<p id="hw-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="hw-label">Detecting hardware wallet...</span></p>
|
||||
|
||||
<p id="check-loading" style="display:none;"><span class="fa fa-question-circle" style="color:orange"></span> <span class="check-label">Detecting hardware wallet...</span></p>
|
||||
<p id="check-error" style="display:none;"><span class="fa fa-times-circle" style="color:red;"></span> <span class="check-label">An error happened</span></p>
|
||||
<p id="check-success" style="display:none;"><span class="fa fa-check-circle" style="color:green;"></span> <span class="check-label">Detecting hardware wallet...</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -53,6 +53,17 @@
|
||||
"wwwroot/checkout/**/*.js"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/lightning-node-info-bundle.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/clipboard.js/clipboard.js",
|
||||
"wwwroot/vendor/jquery/jquery.js",
|
||||
"wwwroot/vendor/vuejs/vue.min.js",
|
||||
"wwwroot/vendor/vuejs/vue-qrcode.js",
|
||||
"wwwroot/vendor/vue-toasted/vue-toasted.min.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/cart-bundle.min.js",
|
||||
"inputFiles": [
|
||||
@ -61,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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
128
BTCPayServer/wwwroot/cart/css/style.css
Normal file
128
BTCPayServer/wwwroot/cart/css/style.css
Normal file
@ -0,0 +1,128 @@
|
||||
.modal-content {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.modal-header {
|
||||
border-top-left-radius: 0.4rem;
|
||||
border-top-right-radius: 0.4rem;
|
||||
}
|
||||
|
||||
.card-img-top {
|
||||
width: 100%;
|
||||
max-height: 180px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.js-cart-added {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 0.25rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: none;
|
||||
}
|
||||
.js-cart-added .fa {
|
||||
height: 50px;
|
||||
position: relative;
|
||||
top: 50%;
|
||||
margin-top: -25px;
|
||||
}
|
||||
|
||||
.js-add-cart:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#js-cart-confirm {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------
|
||||
SIDEBAR STYLE
|
||||
----------------------------------------------------- */
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
width: 400px;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
z-index: 999;
|
||||
background: #e1e6ea;
|
||||
transition: all 0.3s;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
#sidebar .js-cart {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
#sidebar.active {
|
||||
margin-right: -400px;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------
|
||||
CONTENT STYLE
|
||||
----------------------------------------------------- */
|
||||
|
||||
#content {
|
||||
width: calc(100% - 400px);
|
||||
min-height: 100vh;
|
||||
transition: all 0.3s;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#content.active {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bg-gray {
|
||||
background-color: #aaa;
|
||||
}
|
||||
.text-black {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------
|
||||
MEDIAQUERIES
|
||||
----------------------------------------------------- */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#sidebar {
|
||||
margin-right: -400px;
|
||||
}
|
||||
#sidebar .js-cart {
|
||||
display: inline;
|
||||
}
|
||||
#sidebar.active {
|
||||
margin-right: 0;
|
||||
}
|
||||
#content {
|
||||
width: 100%;
|
||||
}
|
||||
#content.active {
|
||||
width: calc(100% - 400px);
|
||||
}
|
||||
#sidebarCollapse span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
#sidebar {
|
||||
width: 100%;
|
||||
margin-right: -575px;
|
||||
}
|
||||
#content.active {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
@ -1,28 +1,48 @@
|
||||
$.fn.addAnimate = function(completeCallback) {
|
||||
var documentHeight = $(document).height(),
|
||||
itemPos = $(this).offset(),
|
||||
itemY = itemPos.top,
|
||||
cartPos = $('#js-cart').find('.badge').position();
|
||||
tempItem = '<span id="js-cart-temp-item" class="badge badge-primary text-white badge-pill " style="' +
|
||||
'position: absolute;' +
|
||||
'top: ' + itemPos.top + 'px;' +
|
||||
'left: ' + (itemPos.left + 50) + 'px;">'+
|
||||
'<i class="fa fa-shopping-basket"></i></span>';
|
||||
if ($(this).find('.js-cart-added').length === 0) {
|
||||
$(this).append('<div class="js-cart-added"><i class="fa fa-check fa-3x text-white align-middle"></i></div>');
|
||||
|
||||
// Animate the element
|
||||
$(this).find('.js-cart-added').fadeIn(200, function(){
|
||||
var self = this;
|
||||
// Show it for 200ms
|
||||
setTimeout(function(){
|
||||
// Hide and remove
|
||||
$(self).fadeOut(100, function(){
|
||||
$(this).remove();
|
||||
|
||||
// Make animation speed look constant regardless of how far the object is from the cart
|
||||
var animationSpeed = (Math.log(itemY) * (documentHeight / Math.log2(documentHeight - itemY))) / 2;
|
||||
completeCallback && completeCallback();
|
||||
})
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add the cart item badge and animate it
|
||||
$('body').after(tempItem);
|
||||
$('#js-cart-temp-item').animate({
|
||||
easing: 'swing',
|
||||
top: cartPos.top,
|
||||
left: cartPos.left
|
||||
}, animationSpeed, function() {
|
||||
$(this).remove();
|
||||
completeCallback && completeCallback();
|
||||
});
|
||||
};
|
||||
function removeAccents(input){
|
||||
var accents = 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇČçčÐĎďÌÍÎÏìíîïĽľÙÚÛÜùúûüÑŇñňŠšŤťŸÿýŽž ́',
|
||||
accentsOut = 'AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCCccDDdIIIIiiiiLlUUUUuuuuNNnnSsTtYyyZz ',
|
||||
output = '',
|
||||
index = -1;
|
||||
|
||||
for( var i = 0; i < input.length; i++ ) {
|
||||
index = accents.indexOf(input[i]);
|
||||
|
||||
if( index != -1 ) {
|
||||
output += typeof accentsOut[index] != 'undefined' ? accentsOut[index] : '';
|
||||
}
|
||||
else {
|
||||
output += typeof input[i] != 'undefined' ? input[i] : '';
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
jQuery.expr[':'].icontains = function (a, i, m) {
|
||||
var string = removeAccents(jQuery(a).text().toLowerCase());
|
||||
|
||||
return string.indexOf(removeAccents(m[3].toLowerCase())) >= 0;
|
||||
};
|
||||
|
||||
$(document).ready(function(){
|
||||
var cart = new Cart();
|
||||
@ -31,27 +51,86 @@ $(document).ready(function(){
|
||||
event.preventDefault();
|
||||
|
||||
var $btn = $(event.target),
|
||||
self = this;
|
||||
id = $btn.closest('.card').data('id'),
|
||||
item = srvModel.items[id];
|
||||
item = srvModel.items[id],
|
||||
items = cart.items;
|
||||
|
||||
// Is event catching disabled?
|
||||
if (!$(this).hasClass('disabled')) {
|
||||
// Disable catching events for this element
|
||||
$(this).addClass('disabled');
|
||||
|
||||
// Add-to-cart animation only once
|
||||
$(this).addAnimate(function(){
|
||||
// Enable the event
|
||||
$(self).removeClass('disabled');
|
||||
});
|
||||
|
||||
// Animate adding and then add then save
|
||||
$(this).addAnimate(function(){
|
||||
cart.addItem({
|
||||
id: id,
|
||||
title: item.title,
|
||||
price: item.price,
|
||||
image: typeof item.image != 'underfined' ? item.image : null
|
||||
});
|
||||
});
|
||||
cart.listItems();
|
||||
}
|
||||
});
|
||||
|
||||
// Destroy the cart when the "pay button is clicked"
|
||||
$('#js-cart-pay').click(function(){
|
||||
cart.destroy();
|
||||
cart.destroy(true);
|
||||
});
|
||||
|
||||
// Repopulate cart items in the modal when it opens
|
||||
$('#cartModal').on('show.bs.modal', function () {
|
||||
cart.listItems();
|
||||
$('.js-cart').on('click', function () {
|
||||
$('#sidebar, #content').toggleClass('active');
|
||||
$('.collapse.in').toggleClass('in');
|
||||
$('a[aria-expanded=true]').attr('aria-expanded', 'false');
|
||||
});
|
||||
|
||||
$('.js-search').keyup(function(event){
|
||||
var str = $(this).val();
|
||||
|
||||
$('#js-pos-list').find(".card-wrapper").show();
|
||||
|
||||
if (str.length > 1) {
|
||||
var $list = $('#js-pos-list').find(".card-title:not(:icontains('" + str + "'))");
|
||||
$list.parents('.card-wrapper').hide();
|
||||
$('.js-search-reset').show();
|
||||
}
|
||||
});
|
||||
|
||||
$('.js-search-reset').click(function(event){
|
||||
event.preventDefault();
|
||||
|
||||
$('.js-search').val('');
|
||||
$('.js-search').trigger('keyup');
|
||||
$(this).hide();
|
||||
});
|
||||
|
||||
$('#js-cart-summary').find('tbody').prepend(cart.template($('#template-cart-tip'), {
|
||||
'tip': cart.fromCents(cart.getTip()) || ''
|
||||
}));
|
||||
|
||||
$('#cartModal').one('show.bs.modal', function () {
|
||||
cart.updateDiscount();
|
||||
cart.updateTip();
|
||||
cart.updateSummaryProducts();
|
||||
cart.updateSummaryTotal();
|
||||
|
||||
// Change total when tip is changed
|
||||
$('.js-cart-tip').inputAmount(cart, 'tip');
|
||||
// Remove tip
|
||||
$('.js-cart-tip-remove').removeAmount(cart, 'tip');
|
||||
|
||||
$('.js-cart-tip-btn').click(function(event){
|
||||
event.preventDefault();
|
||||
|
||||
var $tip = $('.js-cart-tip'),
|
||||
discount = cart.percentage(cart.getTotalProducts(), cart.getDiscount());
|
||||
|
||||
$tip.val(cart.percentage(cart.getTotalProducts() - discount, parseInt($(this).data('tip'))));
|
||||
$tip.trigger('input');
|
||||
});
|
||||
});
|
||||
});
|
@ -2,126 +2,313 @@ function Cart() {
|
||||
this.items = 0;
|
||||
this.totalAmount = 0;
|
||||
this.content = [];
|
||||
this.tip = 0;
|
||||
|
||||
this.loadLocalStorage();
|
||||
this.itemsCount();
|
||||
this.buildUI();
|
||||
|
||||
this.$list = $('#js-cart-list');
|
||||
this.$items = $('#js-cart-items');
|
||||
this.$total = $('.js-cart-total');
|
||||
this.$summaryProducts = $('.js-cart-summary-products');
|
||||
this.$summaryDiscount = $('.js-cart-summary-discount');
|
||||
this.$summaryTotal = $('.js-cart-summary-total');
|
||||
this.$summaryTip = $('.js-cart-summary-tip');
|
||||
this.$destroy = $('.js-cart-destroy');
|
||||
this.$confirm = $('#js-cart-confirm');
|
||||
|
||||
this.listItems();
|
||||
this.bindEmptyCart();
|
||||
|
||||
this.updateItemsCount();
|
||||
this.updateAmount();
|
||||
}
|
||||
|
||||
Cart.prototype.addItem = function(item) {
|
||||
// Increment the existing item count
|
||||
var result = this.content.filter(function(obj){
|
||||
if (obj.id === item.id){
|
||||
obj.count++;
|
||||
Cart.prototype.setCustomAmount = function(amount) {
|
||||
this.customAmount = this.toNumber(amount);
|
||||
|
||||
if (this.customAmount > 0) {
|
||||
localStorage.setItem(this.getStorageKey('cartCustomAmount'), this.customAmount);
|
||||
} else {
|
||||
localStorage.removeItem(this.getStorageKey('cartCustomAmount'));
|
||||
}
|
||||
return this.customAmount;
|
||||
}
|
||||
|
||||
Cart.prototype.getCustomAmount = function() {
|
||||
return this.toCents(this.customAmount);
|
||||
}
|
||||
|
||||
Cart.prototype.setTip = function(amount) {
|
||||
this.tip = this.toNumber(amount);
|
||||
|
||||
if (this.tip > 0) {
|
||||
localStorage.setItem(this.getStorageKey('cartTip'), this.tip);
|
||||
} else {
|
||||
localStorage.removeItem(this.getStorageKey('cartTip'));
|
||||
}
|
||||
return this.tip;
|
||||
}
|
||||
|
||||
Cart.prototype.getTip = function() {
|
||||
return this.toCents(this.tip);
|
||||
}
|
||||
|
||||
Cart.prototype.setDiscount = function(amount) {
|
||||
this.discount = this.toNumber(amount);
|
||||
|
||||
if (this.discount > 0) {
|
||||
localStorage.setItem(this.getStorageKey('cartDiscount'), this.discount);
|
||||
} else {
|
||||
localStorage.removeItem(this.getStorageKey('cartDiscount'));
|
||||
}
|
||||
return this.discount;
|
||||
}
|
||||
|
||||
Cart.prototype.getDiscount = function() {
|
||||
return this.toCents(this.discount);
|
||||
}
|
||||
|
||||
Cart.prototype.getDiscountAmount = function(amount) {
|
||||
return this.percentage(amount, this.getDiscount());
|
||||
}
|
||||
|
||||
// Get total amount of products
|
||||
Cart.prototype.getTotalProducts = function() {
|
||||
var amount = 0 ;
|
||||
|
||||
// Always calculate the total amount based on the cart content
|
||||
for (var key in this.content) {
|
||||
if (
|
||||
this.content.hasOwnProperty(key) &&
|
||||
typeof this.content[key] != 'undefined' &&
|
||||
!this.content[key].disabled
|
||||
) {
|
||||
var price = this.toCents(this.content[key].price.value);
|
||||
amount += (this.content[key].count * price);
|
||||
}
|
||||
|
||||
return obj.id === item.id
|
||||
});
|
||||
}
|
||||
|
||||
// Add custom amount
|
||||
amount += this.getCustomAmount();
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
// Get absolute total amount
|
||||
Cart.prototype.getTotal = function(includeTip) {
|
||||
this.totalAmount = this.getTotalProducts();
|
||||
|
||||
if (this.getDiscount() > 0) {
|
||||
this.totalAmount -= this.getDiscountAmount(this.totalAmount);
|
||||
}
|
||||
|
||||
if (includeTip) {
|
||||
this.totalAmount += this.getTip();
|
||||
}
|
||||
|
||||
return this.fromCents(this.totalAmount);
|
||||
}
|
||||
|
||||
/*
|
||||
* Data manipulation
|
||||
*/
|
||||
// Add item to the cart or update its count
|
||||
Cart.prototype.addItem = function(item) {
|
||||
var id = item.id,
|
||||
result = this.content.filter(function(obj){
|
||||
return obj.id === id;
|
||||
});
|
||||
|
||||
// Add new item because it doesn't exist yet
|
||||
if (!result.length) {
|
||||
this.content.push({id: item.id, title: item.title, price: item.price, count: 1, image: item.image})
|
||||
this.content.push({id: id, title: item.title, price: item.price, count: 0, image: item.image});
|
||||
this.emptyCartToggle();
|
||||
}
|
||||
|
||||
this.items++;
|
||||
this.saveLocalStorage();
|
||||
this.itemsCount();
|
||||
this.updateTotal();
|
||||
this.updateAmount();
|
||||
// Increment item count
|
||||
this.incrementItem(id);
|
||||
}
|
||||
|
||||
Cart.prototype.incrementItem = function(id) {
|
||||
var self = this;
|
||||
this.items = 0; // Calculate total # of items from scratch just to make sure
|
||||
|
||||
this.content.filter(function(obj){
|
||||
// Increment the item count
|
||||
if (obj.id === id){
|
||||
obj.count++;
|
||||
delete(obj.disabled);
|
||||
}
|
||||
|
||||
// Increment the total # of items
|
||||
self.items += obj.count;
|
||||
});
|
||||
|
||||
this.updateAll();
|
||||
}
|
||||
|
||||
// Disable cart item so it doesn't count towards total amount
|
||||
Cart.prototype.disableItem = function(id) {
|
||||
var self = this;
|
||||
|
||||
this.content.filter(function(obj){
|
||||
if (obj.id === id){
|
||||
obj.disabled = true;
|
||||
self.items -= obj.count;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateAll();
|
||||
}
|
||||
|
||||
// Enable cart item so it counts towards total amount
|
||||
Cart.prototype.enableItem = function(id) {
|
||||
var self = this;
|
||||
|
||||
this.content.filter(function(obj){
|
||||
if (obj.id === id){
|
||||
delete(obj.disabled);
|
||||
self.items += obj.count;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateAll();
|
||||
}
|
||||
|
||||
Cart.prototype.decrementItem = function(id) {
|
||||
var self = this;
|
||||
this.items = 0; // Calculate total # of items from scratch just to make sure
|
||||
|
||||
// Decrement the existing item count
|
||||
this.content.filter(function(obj, index, arr){
|
||||
// Decrement the item count
|
||||
if (obj.id === id)
|
||||
{
|
||||
obj.count--;
|
||||
delete(obj.disabled);
|
||||
|
||||
// It's the last item with the same ID, remove it
|
||||
if (obj.count === 0) {
|
||||
if (obj.count <= 0) {
|
||||
self.removeItem(id, index, arr);
|
||||
}
|
||||
}
|
||||
|
||||
self.items += obj.count;
|
||||
});
|
||||
|
||||
this.items--;
|
||||
this.saveLocalStorage();
|
||||
this.itemsCount();
|
||||
this.updateTotal();
|
||||
this.updateAmount();
|
||||
|
||||
if (this.items === 0) {
|
||||
this.emptyList();
|
||||
}
|
||||
this.updateAll();
|
||||
}
|
||||
|
||||
Cart.prototype.removeItemAll = function(id) {
|
||||
var self = this;
|
||||
|
||||
this.content.filter(function(obj, index, arr){
|
||||
if (obj.id === id)
|
||||
{
|
||||
self.removeItem(id, index, arr);
|
||||
|
||||
for (var i = 0; i < obj.count; i++) {
|
||||
self.items--;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.saveLocalStorage();
|
||||
this.itemsCount();
|
||||
this.updateTotal();
|
||||
this.updateAmount();
|
||||
this.items = 0;
|
||||
|
||||
if (this.items === 0) {
|
||||
this.emptyList();
|
||||
// Remove by item
|
||||
if (typeof id != 'undefined') {
|
||||
this.content.filter(function(obj, index, arr){
|
||||
if (obj.id === id) {
|
||||
self.removeItem(id, index, arr);
|
||||
|
||||
for (var i = 0; i < obj.count; i++) {
|
||||
self.items--;
|
||||
}
|
||||
}
|
||||
|
||||
self.items += obj.count;
|
||||
});
|
||||
} else { // Remove all
|
||||
this.$list.find('tbody').empty();
|
||||
this.content = [];
|
||||
}
|
||||
|
||||
this.emptyCartToggle();
|
||||
this.updateAll();
|
||||
}
|
||||
|
||||
Cart.prototype.removeItem = function(id, index, arr) {
|
||||
// Remove from the array
|
||||
arr.splice(index, 1);
|
||||
// Remove from the DOM
|
||||
$('#js-cart-list').find('tr').eq(index+1).remove();
|
||||
this.$list.find('tr').eq(index+1).remove();
|
||||
}
|
||||
|
||||
Cart.prototype.setTip = function(tip) {
|
||||
return this.tip = tip;
|
||||
/*
|
||||
* Update DOM
|
||||
*/
|
||||
// Update all data elements
|
||||
Cart.prototype.updateAll = function() {
|
||||
this.saveLocalStorage();
|
||||
this.updateItemsCount();
|
||||
this.updateDiscount();
|
||||
this.updateSummaryProducts();
|
||||
this.updateSummaryTotal();
|
||||
this.updateTotal();
|
||||
this.updateAmount();
|
||||
}
|
||||
|
||||
Cart.prototype.itemsCount = function() {
|
||||
$('#js-cart-items').text(this.items);
|
||||
// Update number of cart items
|
||||
Cart.prototype.updateItemsCount = function() {
|
||||
this.$items.text(this.items);
|
||||
}
|
||||
|
||||
Cart.prototype.getTotal = function(plain) {
|
||||
this.totalAmount = 0;
|
||||
// Update total products (including the custom amount and discount) in the cart
|
||||
Cart.prototype.updateTotal = function() {
|
||||
this.$total.text(this.formatCurrency(this.getTotal()));
|
||||
}
|
||||
|
||||
// Always calculate the total amount based on the cart content
|
||||
for (var key in this.content) {
|
||||
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined') {
|
||||
var price = this.toCents(this.content[key].price.value);
|
||||
this.totalAmount += (this.content[key].count * price);
|
||||
}
|
||||
// Update total amount in the summary
|
||||
Cart.prototype.updateSummaryTotal = function() {
|
||||
this.$summaryTotal.text(this.formatCurrency(this.getTotal(true)));
|
||||
}
|
||||
|
||||
// Update total products amount in the summary
|
||||
Cart.prototype.updateSummaryProducts = function() {
|
||||
this.$summaryProducts.text(this.formatCurrency(this.fromCents(this.getTotalProducts())));
|
||||
}
|
||||
|
||||
// Update discount amount in the summary
|
||||
Cart.prototype.updateDiscount = function(amount) {
|
||||
var discount = 0;
|
||||
|
||||
if (typeof amount != 'undefined') {
|
||||
discount = amount;
|
||||
} else {
|
||||
discount = this.percentage(this.getTotalProducts(), this.getDiscount());
|
||||
discount = this.fromCents(discount);
|
||||
}
|
||||
|
||||
this.totalAmount += this.toCents(this.tip);
|
||||
|
||||
return this.fromCents(this.totalAmount);
|
||||
this.$summaryDiscount.text((discount > 0 ? '-' : '') + this.formatCurrency(discount));
|
||||
}
|
||||
|
||||
Cart.prototype.updateTotal = function() {
|
||||
$('#js-cart-total').text(this.formatCurrency(this.getTotal(), srvModel.currencyCode, srvModel.currencySymbol));
|
||||
// Update tip amount in the summary
|
||||
Cart.prototype.updateTip = function(amount) {
|
||||
var tip = typeof amount != 'undefined' ? amount : this.fromCents(this.getTip());
|
||||
|
||||
this.$summaryTip.text(this.formatCurrency(tip));
|
||||
}
|
||||
|
||||
// Update hidden total amount value to be sent to the checkout page
|
||||
Cart.prototype.updateAmount = function() {
|
||||
$('#js-cart-amount').val(this.getTotal());
|
||||
$('#js-cart-amount').val(this.getTotal(true));
|
||||
}
|
||||
|
||||
Cart.prototype.resetDiscount = function() {
|
||||
this.setDiscount(0);
|
||||
this.updateDiscount(0);
|
||||
$('.js-cart-discount').val('');
|
||||
}
|
||||
|
||||
Cart.prototype.resetTip = function() {
|
||||
this.setTip(0);
|
||||
this.updateTip(0);
|
||||
$('.js-cart-tip').val('');
|
||||
}
|
||||
|
||||
Cart.prototype.resetCustomAmount = function() {
|
||||
this.setCustomAmount(0);
|
||||
$('.js-cart-custom-amount').val('');
|
||||
}
|
||||
|
||||
// Escape html characters
|
||||
Cart.prototype.escape = function(input) {
|
||||
return ('' + input) /* Forces the conversion to string. */
|
||||
.replace(/&/g, '&') /* This MUST be the 1st replacement. */
|
||||
@ -132,8 +319,51 @@ Cart.prototype.escape = function(input) {
|
||||
;
|
||||
}
|
||||
|
||||
// Load the template
|
||||
Cart.prototype.template = function($template, obj) {
|
||||
var template = $template.text();
|
||||
|
||||
for (var key in obj) {
|
||||
var re = new RegExp('{' + key + '}', 'mg');
|
||||
template = template.replace(re, obj[key]);
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
// Build the cart skeleton
|
||||
Cart.prototype.buildUI = function() {
|
||||
var $table = $('#js-cart-extra').find('tbody'),
|
||||
list = [];
|
||||
|
||||
tableTemplate = this.template($('#template-cart-extra'), {
|
||||
'discount': this.escape(this.fromCents(this.getDiscount()) || ''),
|
||||
'customAmount': this.escape(this.fromCents(this.getCustomAmount()) || '')
|
||||
});
|
||||
list.push($(tableTemplate));
|
||||
|
||||
tableTemplate = this.template($('#template-cart-total'), {
|
||||
'total': this.escape(this.formatCurrency(this.getTotal()))
|
||||
});
|
||||
list.push($(tableTemplate));
|
||||
|
||||
// Add the list to DOM
|
||||
$table.append(list);
|
||||
|
||||
// Change total when discount is changed
|
||||
$('.js-cart-discount').inputAmount(this, 'discount');
|
||||
// Remove discount
|
||||
$('.js-cart-discount-remove').removeAmount(this, 'discount');
|
||||
|
||||
// Change total when discount is changed
|
||||
$('.js-cart-custom-amount').inputAmount(this, 'customAmount');
|
||||
// Remove discount
|
||||
$('.js-cart-custom-amount-remove').removeAmount(this, 'customAmount');
|
||||
}
|
||||
|
||||
// List cart items and bind their events
|
||||
Cart.prototype.listItems = function() {
|
||||
var $table = $('#js-cart-list').find('tbody'),
|
||||
var $table = this.$list.find('tbody'),
|
||||
self = this,
|
||||
list = [],
|
||||
tableTemplate = '';
|
||||
@ -142,74 +372,74 @@ Cart.prototype.listItems = function() {
|
||||
// Prepare the list of items in the cart
|
||||
for (var key in this.content) {
|
||||
var item = this.content[key],
|
||||
id = this.escape(item.id),
|
||||
title = this.escape(item.title),
|
||||
image = this.escape(item.image),
|
||||
count = this.escape(item.count),
|
||||
price = this.escape(item.price.formatted),
|
||||
total = this.escape(this.formatCurrency(this.getTotal(), srvModel.currencyCode, srvModel.currencySymbol)),
|
||||
step = this.escape(srvModel.step),
|
||||
tip = this.escape(this.tip || ''),
|
||||
customTipText = this.escape(srvModel.customTipText);
|
||||
image = this.escape(item.image);
|
||||
|
||||
tableTemplate = '<tr data-id="' + id + '">' +
|
||||
(image !== null ? '<td class="align-middle pr-0" width="60"><img src="' + image + '" width="100%"></td>' : '') +
|
||||
'<td class="align-middle pr-0"><b>' + title + '</b></td>' +
|
||||
'<td class="align-middle pr-0" align="right"><div class="input-group">' +
|
||||
' <input class="js-cart-item-count form-control form-control-sm pull-left" type="number" min="0" step="1" name="count" placeholder="Qty" value="' + count + '" data-prev="' + count + '">' +
|
||||
' <div class="input-group-append"><a class="js-cart-item-remove btn btn-danger btn-sm" href="#"><i class="fa fa-remove"></i></a></div>' +
|
||||
'</div></td>' +
|
||||
'<td class="align-middle" align="right">' + price + '</td>' +
|
||||
'</tr>';
|
||||
tableTemplate = this.template($('#template-cart-item'), {
|
||||
'id': this.escape(item.id),
|
||||
'image': image ? this.template($('#template-cart-item-image'), {
|
||||
'image' : image
|
||||
}) : '',
|
||||
'title': this.escape(item.title),
|
||||
'count': this.escape(item.count),
|
||||
'price': this.escape(item.price.formatted)
|
||||
});
|
||||
list.push($(tableTemplate));
|
||||
}
|
||||
|
||||
tableTemplate = '<tr><td colspan="4"><div class="row"><div class="col-sm-7 py-2">' + customTipText + '</div><div class="col-sm-5">' +
|
||||
'<div class="input-group">' +
|
||||
'<div class="input-group-prepend">' +
|
||||
'<span class="input-group-text"><i class="fa fa-money"></i></span>' +
|
||||
'</div>' +
|
||||
'<input class="js-cart-tip form-control" type="number" min="0" step="' + step + '" value="' + tip + '" name="tip" placeholder="Amount">' +
|
||||
'</div>' +
|
||||
'</div></div></td></tr>';
|
||||
list.push($(tableTemplate));
|
||||
|
||||
tableTemplate = '<tr class="bg-light h4"><td colspan="1">Total</td><td colspan="3" align="right"><span id="js-cart-total">' + total + '</span></td></tr>';
|
||||
list.push($(tableTemplate));
|
||||
|
||||
// Add the list to DOM
|
||||
$table.html(list);
|
||||
list = [];
|
||||
|
||||
// Update the cart when number of items is changed
|
||||
$('.js-cart-item-count').off().on('input', function(event){
|
||||
var _this = this,
|
||||
id = $(this).closest('tr').data('id'),
|
||||
count = parseInt($(this).val()),
|
||||
prevCount = parseInt($(this).data('prev')),
|
||||
increased = count > prevCount;
|
||||
qty = parseInt($(this).val()),
|
||||
isQty = !isNaN(qty),
|
||||
prevQty = parseInt($(this).data('prev')),
|
||||
qtyDiff = Math.abs(qty - prevQty),
|
||||
qtyIncreased = qty > prevQty;
|
||||
|
||||
// User hasn't inputed any number so stop here
|
||||
if (isNaN(count)) {
|
||||
return false;
|
||||
if (isQty) {
|
||||
$(this).data('prev', qty);
|
||||
} else {
|
||||
// User hasn't inputed any quantity
|
||||
qty = null;
|
||||
}
|
||||
|
||||
$(this).data('prev', count);
|
||||
self.resetTip();
|
||||
|
||||
var item = self.content.filter(function(obj){
|
||||
return obj.id === id
|
||||
});
|
||||
|
||||
// Must be in the loop because user may change the count manually by more than 1
|
||||
for (var i = 0; i < Math.abs(count - prevCount); i++) {
|
||||
if (increased) {
|
||||
// Quantity was increased
|
||||
if (qtyIncreased) {
|
||||
var item = self.content.filter(function(obj){
|
||||
return obj.id === id;
|
||||
});
|
||||
|
||||
// Quantity may have been increased by more than one
|
||||
for (var i = 0; i < qtyDiff; i++) {
|
||||
self.addItem({
|
||||
id: id,
|
||||
title: item.title,
|
||||
price: item.price,
|
||||
image: typeof item.image != 'underfined' ? item.image : null
|
||||
});
|
||||
}
|
||||
} else if (!qtyIncreased) { // Quantity decreased
|
||||
// No quantity set (e.g. empty string)
|
||||
if (!isQty) {
|
||||
// Disable the item so it doesn't count towards total amount
|
||||
self.disableItem(id);
|
||||
} else {
|
||||
self.decrementItem(id);
|
||||
// Quantity vas decreased
|
||||
if (qtyDiff > 0) {
|
||||
// Quantity may have been decreased by more than one
|
||||
for (var i = 0; i < qtyDiff; i++) {
|
||||
self.decrementItem(id);
|
||||
}
|
||||
} else {
|
||||
// Quantity hasn't changed, enable the item so it counts towards the total amount
|
||||
self.enableItem(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -218,29 +448,71 @@ Cart.prototype.listItems = function() {
|
||||
$('.js-cart-item-remove').off().on('click', function(event){
|
||||
event.preventDefault();
|
||||
|
||||
var id = $(this).closest('tr').data('id');
|
||||
|
||||
self.removeItemAll(id);
|
||||
self.resetTip();
|
||||
self.removeItemAll($(this).closest('tr').data('id'));
|
||||
});
|
||||
|
||||
// Change total when tip is changed
|
||||
$('.js-cart-tip').off().on('input', function(event){
|
||||
self.setTip($(this).val());
|
||||
self.updateTotal();
|
||||
self.updateAmount();
|
||||
// Increment item
|
||||
$('.js-cart-item-plus').off().on('click', function(event){
|
||||
event.preventDefault();
|
||||
|
||||
var $val = $(this).parents('.input-group').find('.js-cart-item-count'),
|
||||
val = parseInt($val.val() || $val.data('prev')) + 1;
|
||||
|
||||
$val.val(val);
|
||||
$val.data('prev', val);
|
||||
self.resetTip();
|
||||
self.incrementItem($(this).closest('tr').data('id'));
|
||||
});
|
||||
|
||||
// Decrement item
|
||||
$('.js-cart-item-minus').off().on('click', function(event){
|
||||
event.preventDefault();
|
||||
|
||||
var $val = $(this).parents('.input-group').find('.js-cart-item-count'),
|
||||
id = $(this).closest('tr').data('id'),
|
||||
val = parseInt($val.val() || $val.data('prev')) - 1;
|
||||
|
||||
self.resetTip();
|
||||
|
||||
if (val === 0) {
|
||||
self.removeItemAll(id);
|
||||
} else {
|
||||
$val.val(val);
|
||||
$val.data('prev', val);
|
||||
self.decrementItem(id);
|
||||
}
|
||||
});
|
||||
} else { // No item in the cart
|
||||
self.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
Cart.prototype.emptyList = function() {
|
||||
var $table = $('#js-cart-list').find('tbody');
|
||||
Cart.prototype.bindEmptyCart = function() {
|
||||
var self = this;
|
||||
|
||||
$table.html('<tr><td colspan="4">The cart is empty.</td></tr>');
|
||||
this.emptyCartToggle();
|
||||
|
||||
this.$destroy.click(function(event){
|
||||
event.preventDefault();
|
||||
|
||||
self.destroy();
|
||||
self.emptyCartToggle();
|
||||
});
|
||||
}
|
||||
|
||||
Cart.prototype.formatCurrency = function(amount, currency, symbol) {
|
||||
Cart.prototype.emptyCartToggle = function() {
|
||||
if (this.content.length > 0 || this.getCustomAmount()) {
|
||||
this.$destroy.show();
|
||||
this.$confirm.removeAttr('disabled');
|
||||
} else {
|
||||
this.$destroy.hide();
|
||||
this.$confirm.attr('disabled', 'disabled');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Currencies and numbers
|
||||
*/
|
||||
Cart.prototype.formatCurrency = function(amount) {
|
||||
var amt = '',
|
||||
thousandsSep = '',
|
||||
decimalSep = ''
|
||||
@ -252,12 +524,14 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) {
|
||||
if (srvModel.currencyInfo.symbolSpace) {
|
||||
prefix = prefix + ' ';
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
postfix = srvModel.currencyInfo.currencySymbol;
|
||||
if (srvModel.currencyInfo.symbolSpace) {
|
||||
postfix = ' ' + postfix;
|
||||
}
|
||||
|
||||
}
|
||||
thousandsSep = srvModel.currencyInfo.thousandSeparator;
|
||||
decimalSep = srvModel.currencyInfo.decimalSeparator;
|
||||
@ -267,8 +541,9 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) {
|
||||
var splittedAmount = amt.split('.');
|
||||
amt = (splittedAmount[0] + '.').replace(/(\d)(?=(\d{3})+\.)/g, '$1' + thousandsSep);
|
||||
amt = amt.substr(0, amt.length - 1);
|
||||
if(splittedAmount.length == 2)
|
||||
amt = amt + '.' + splittedAmount[1];
|
||||
if(splittedAmount.length == 2) {
|
||||
amt = amt + decimalSep + splittedAmount[1];
|
||||
}
|
||||
if (srvModel.currencyInfo.divisibility !== 0) {
|
||||
amt[amt.length - srvModel.currencyInfo.divisibility - 1] = decimalSep;
|
||||
}
|
||||
@ -277,6 +552,10 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) {
|
||||
return amt;
|
||||
}
|
||||
|
||||
Cart.prototype.toNumber = function(num) {
|
||||
return (num * 1) || 0;
|
||||
}
|
||||
|
||||
Cart.prototype.toCents = function(num) {
|
||||
return num * Math.pow(10, srvModel.currencyInfo.divisibility);
|
||||
}
|
||||
@ -285,27 +564,108 @@ Cart.prototype.fromCents = function(num) {
|
||||
return num / Math.pow(10, srvModel.currencyInfo.divisibility);
|
||||
}
|
||||
|
||||
Cart.prototype.getStorageKey = function () { return ('cart' + srvModel.appId + srvModel.currencyCode); }
|
||||
Cart.prototype.percentage = function(amount, percentage) {
|
||||
return this.fromCents((amount / 100) * percentage);
|
||||
}
|
||||
|
||||
/*
|
||||
* Storage
|
||||
*/
|
||||
Cart.prototype.getStorageKey = function (name) {
|
||||
return (name + srvModel.appId + srvModel.currencyCode);
|
||||
}
|
||||
|
||||
Cart.prototype.saveLocalStorage = function() {
|
||||
localStorage.setItem(this.getStorageKey(), JSON.stringify(this.content));
|
||||
localStorage.setItem(this.getStorageKey('cart'), JSON.stringify(this.content));
|
||||
}
|
||||
|
||||
Cart.prototype.loadLocalStorage = function() {
|
||||
this.content = $.parseJSON(localStorage.getItem(this.getStorageKey())) || [];
|
||||
this.content = $.parseJSON(localStorage.getItem(this.getStorageKey('cart'))) || [];
|
||||
|
||||
// Get number of cart items
|
||||
for (var key in this.content) {
|
||||
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined' && this.content[key] != null) {
|
||||
this.items += this.content[key].count;
|
||||
|
||||
// Delete the disabled flag if any
|
||||
delete(this.content[key].disabled);
|
||||
}
|
||||
}
|
||||
|
||||
this.discount = localStorage.getItem(this.getStorageKey('cartDiscount'));
|
||||
this.customAmount = localStorage.getItem(this.getStorageKey('cartCustomAmount'));
|
||||
this.tip = localStorage.getItem(this.getStorageKey('cartTip'));
|
||||
}
|
||||
|
||||
Cart.prototype.destroy = function() {
|
||||
localStorage.removeItem(this.getStorageKey());
|
||||
this.content = [];
|
||||
this.items = 0;
|
||||
this.totalAmount = 0;
|
||||
this.tip = 0;
|
||||
Cart.prototype.destroy = function(keepAmount) {
|
||||
this.resetDiscount();
|
||||
this.resetTip();
|
||||
this.resetCustomAmount();
|
||||
|
||||
// When form is sent
|
||||
if (keepAmount) {
|
||||
this.content = [];
|
||||
this.items = 0;
|
||||
} else {
|
||||
this.removeItemAll();
|
||||
}
|
||||
|
||||
localStorage.removeItem(this.getStorageKey('cart'));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* jQuery helpers
|
||||
*/
|
||||
$.fn.inputAmount = function(obj, type) {
|
||||
$(this).off().on('input', function(event){
|
||||
var val = obj.toNumber($(this).val());
|
||||
|
||||
switch (type) {
|
||||
case 'customAmount':
|
||||
obj.setCustomAmount(val);
|
||||
obj.updateDiscount();
|
||||
obj.updateSummaryProducts();
|
||||
obj.updateTotal();
|
||||
obj.resetTip();
|
||||
break;
|
||||
case 'discount':
|
||||
obj.setDiscount(val);
|
||||
obj.updateDiscount();
|
||||
obj.updateSummaryProducts();
|
||||
obj.updateTotal();
|
||||
obj.resetTip();
|
||||
break;
|
||||
case 'tip':
|
||||
obj.setTip(val);
|
||||
obj.updateTip();
|
||||
break;
|
||||
}
|
||||
|
||||
obj.updateSummaryTotal();
|
||||
obj.updateAmount();
|
||||
obj.emptyCartToggle();
|
||||
});
|
||||
}
|
||||
|
||||
$.fn.removeAmount = function(obj, type) {
|
||||
$(this).off().on('click', function(event){
|
||||
event.preventDefault();
|
||||
|
||||
switch (type) {
|
||||
case 'customAmount':
|
||||
obj.resetCustomAmount();
|
||||
obj.updateSummaryProducts();
|
||||
break;
|
||||
case 'discount':
|
||||
obj.resetDiscount();
|
||||
obj.updateSummaryProducts();
|
||||
break;
|
||||
}
|
||||
|
||||
obj.resetTip();
|
||||
obj.updateTotal();
|
||||
obj.updateSummaryTotal();
|
||||
obj.emptyCartToggle();
|
||||
});
|
||||
}
|
88
BTCPayServer/wwwroot/checkout/coinswitch.html
Normal file
88
BTCPayServer/wwwroot/checkout/coinswitch.html
Normal file
@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>CoinSwitch</title>
|
||||
<script>
|
||||
var script = document.createElement('script'),
|
||||
head = document.head || document.getElementsByTagName('head')[0];
|
||||
script.src = 'https://files.coinswitch.co/public/js/cs_switch.js';
|
||||
head.insertBefore(script, head.firstChild);
|
||||
script.addEventListener('load', function () {
|
||||
var qs = (function (a) {
|
||||
if (a == "") return {};
|
||||
var b = {};
|
||||
for (var i = 0; i < a.length; ++i) {
|
||||
var p = a[i].split('=', 2);
|
||||
if (p.length == 1)
|
||||
b[p[0]] = "";
|
||||
else
|
||||
b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
|
||||
}
|
||||
return b;
|
||||
})(window.location.search.substr(1).split('&'));
|
||||
|
||||
var merchantId = qs["merchant_id"];
|
||||
var toCurrencyDue = qs["toCurrencyDue"];
|
||||
var toCurrencyAddress = qs["toCurrencyAddress"];
|
||||
var toCurrency = qs["toCurrency"];
|
||||
var mode = qs["mode"];
|
||||
document.body.classList.add(mode);
|
||||
var orderId = null;
|
||||
var payment = new Coinswitch(merchantId);
|
||||
if(window.localStorage){
|
||||
orderId= window.localStorage.getItem(toCurrencyAddress);
|
||||
}
|
||||
var config = {
|
||||
to_currency: toCurrency.toLowerCase(),
|
||||
to_currency_address: toCurrencyAddress,
|
||||
to_amount: parseFloat(toCurrencyDue),
|
||||
state: orderId
|
||||
};
|
||||
waitForCoinSwitch();
|
||||
|
||||
function waitForCoinSwitch() {
|
||||
if (typeof payment.open !== "function") {
|
||||
setTimeout(waitForCoinSwitch, 1000);
|
||||
return;
|
||||
}
|
||||
payment.open(config);
|
||||
payment.on("Exchange:Complete", function () {
|
||||
if(window.localStorage){
|
||||
window.localStorage.removeItem(toCurrencyAddress);
|
||||
}
|
||||
window.close();
|
||||
});
|
||||
|
||||
payment.on("Exchange:Initiated", function(orderId) {
|
||||
if(window.localStorage){
|
||||
window.localStorage.setItem(toCurrencyAddress,orderId);
|
||||
}
|
||||
});
|
||||
payment.on("Exchange:Closed", function () {
|
||||
window.close();
|
||||
window.postMessage("popup-closed", "*");
|
||||
})
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
body.popup #CoinSwitchPayment .cs-pay {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body.popup #CoinSwitchPayment .cs-pay__main {
|
||||
width: 100%;
|
||||
min-height: 90vh;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
body.popup #CoinSwitchPayment .cs-pay__dismiss-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
@ -11504,3 +11504,15 @@ low-fee-timeline {
|
||||
#prettydropdown-DefaultLang ul li{
|
||||
width:100% !important;
|
||||
}
|
||||
|
||||
[v-cloak] > * { display:none }
|
||||
[v-cloak]::before { content: "loading…" }
|
||||
|
||||
|
||||
.btn-link {
|
||||
color: #329F80;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
color: #24725B;
|
||||
}
|
||||
|
66
BTCPayServer/wwwroot/checkout/js/coinswitchComponent.js
Normal file
66
BTCPayServer/wwwroot/checkout/js/coinswitchComponent.js
Normal file
@ -0,0 +1,66 @@
|
||||
var CoinSwitchComponent =
|
||||
{
|
||||
props: ["toCurrency", "toCurrencyDue", "toCurrencyAddress", "merchantId", "autoload", "mode"],
|
||||
data: function () {
|
||||
return {
|
||||
opened: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showInlineIFrame: function () {
|
||||
return this.url && this.opened;
|
||||
},
|
||||
url: function () {
|
||||
return window.location.origin + "/checkout/coinswitch.html?" +
|
||||
"&toCurrency=" +
|
||||
this.toCurrency +
|
||||
"&toCurrencyAddress=" +
|
||||
this.toCurrencyAddress +
|
||||
"&toCurrencyDue=" +
|
||||
this.toCurrencyDue +
|
||||
"&mode=" +
|
||||
this.mode +
|
||||
(this.merchantId ? "&merchant_id=" + this.merchantId : "");
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openDialog: function (e) {
|
||||
if (e && e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (this.mode === 'inline') {
|
||||
this.opened = true;
|
||||
|
||||
} else if (this.mode === "popup") {
|
||||
var coinSwitchWindow = window.open(
|
||||
this.url,
|
||||
'CoinSwitch',
|
||||
'width=360,height=650,toolbar=0,menubar=0,location=0,status=1,scrollbars=1,resizable=0,left=0,top=0');
|
||||
coinSwitchWindow.opener = null;
|
||||
coinSwitchWindow.focus();
|
||||
}
|
||||
},
|
||||
closeDialog: function () {
|
||||
if (this.mode === 'inline') {
|
||||
this.opened = false;
|
||||
}
|
||||
},
|
||||
onLoadIframe: function (event) {
|
||||
$("#prettydropdown-DefaultLang").hide();
|
||||
var c = this.closeDialog.bind(this);
|
||||
event.currentTarget.contentWindow.addEventListener("message", function (evt) {
|
||||
if (evt && evt.data == "popup-closed") {
|
||||
c();
|
||||
|
||||
$("#prettydropdown-DefaultLang").show();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
if (this.autoload) {
|
||||
this.openDialog();
|
||||
}
|
||||
}
|
||||
};
|
8
BTCPayServer/wwwroot/crowdfund-admin/main.js
Normal file
8
BTCPayServer/wwwroot/crowdfund-admin/main.js
Normal file
@ -0,0 +1,8 @@
|
||||
hljs.initHighlightingOnLoad();
|
||||
$(document).ready(function() {
|
||||
|
||||
$(".richtext").summernote();
|
||||
$(".datetime").flatpickr({
|
||||
enableTime: true
|
||||
});
|
||||
});
|
297
BTCPayServer/wwwroot/crowdfund/app.js
Normal file
297
BTCPayServer/wwwroot/crowdfund/app.js
Normal file
@ -0,0 +1,297 @@
|
||||
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();
|
||||
}
|
||||
if(onChain){
|
||||
Vue.toasted.show('New payment of ' + amount+ " "+ cryptoCode + " " + (onChain? "On Chain": "LN "), {
|
||||
iconPack: "fontawesome",
|
||||
icon: "plus",
|
||||
duration: 10000
|
||||
} );
|
||||
}else{
|
||||
Vue.toasted.show('New payment of ' + amount+ " "+ cryptoCode + " " + (onChain? "On Chain": "LN "), {
|
||||
iconPack: "fontawesome",
|
||||
icon: "bolt",
|
||||
duration: 10000
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
if(srvModel.disqusEnabled){
|
||||
window.disqus_config = function () {
|
||||
// Replace PAGE_URL with your page's canonical URL variable
|
||||
this.page.url = window.location.href;
|
||||
|
||||
// Replace PAGE_IDENTIFIER with your page's unique identifier variable
|
||||
this.page.identifier = self.srvModel.appId;
|
||||
};
|
||||
|
||||
(function() { // REQUIRED CONFIGURATION VARIABLE: EDIT THE SHORTNAME BELOW
|
||||
var d = document, s = d.createElement('script');
|
||||
|
||||
// IMPORTANT: Replace EXAMPLE with your forum shortname!
|
||||
s.src = "https://"+self.srvModel.disqusShortname+".disqus.com/embed.js";
|
||||
s.async= true;
|
||||
s.setAttribute('data-timestamp', +new Date());
|
||||
(d.head || d.body).appendChild(s);
|
||||
|
||||
var s2 = d.createElement('script');
|
||||
s2.src="//"+self.srvModel.disqusShortname+".disqus.com/count.js";
|
||||
s2.async= true;
|
||||
s.setAttribute('data-timestamp', +new Date());
|
||||
(d.head || d.body).appendChild(s);
|
||||
})();
|
||||
}
|
||||
eventAggregator.$on("info-updated", function (model) {
|
||||
console.warn("UPDATED", self.srvModel, arguments);
|
||||
self.srvModel = model;
|
||||
});
|
||||
eventAggregator.$on("connection-pending", function () {
|
||||
self.connectionStatus = "pending";
|
||||
});
|
||||
eventAggregator.$on("connection-failed", function () {
|
||||
self.connectionStatus = "failed";
|
||||
});
|
||||
eventAggregator.$on("connection-lost", function () {
|
||||
self.connectionStatus = "connection lost";
|
||||
});
|
||||
this.updateComputed();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
17
BTCPayServer/wwwroot/crowdfund/helpers/math.js
Normal file
17
BTCPayServer/wwwroot/crowdfund/helpers/math.js
Normal file
@ -0,0 +1,17 @@
|
||||
Number.prototype.noExponents= function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
str= data[0].replace('.', ''),
|
||||
mag= Number(data[1])+ 1;
|
||||
|
||||
if(mag<0){
|
||||
z= sign + '0.';
|
||||
while(mag++) z += '0';
|
||||
return z + str.replace(/^\-/,'');
|
||||
}
|
||||
mag -= str.length;
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
};
|
65
BTCPayServer/wwwroot/crowdfund/services/audioplayer.js
Normal file
65
BTCPayServer/wwwroot/crowdfund/services/audioplayer.js
Normal file
@ -0,0 +1,65 @@
|
||||
|
||||
|
||||
function playSound(path) {
|
||||
// audio supported?
|
||||
if (typeof window.Audio === 'function') {
|
||||
var audioElem = new Audio(path);
|
||||
|
||||
audioElem.play().catch(function(){
|
||||
debugger;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function playQuakeSound (name){
|
||||
// var path = window.location.protocol +"://github.com/ClaudiuHKS/AdvancedQuakeSounds/blob/master/sound/QuakeSounds/"+name+"?raw=true"
|
||||
var path = window.location.protocol + "//github.com/ClaudiuHKS/AdvancedQuakeSounds/raw/master/sound/QuakeSounds/" + name;
|
||||
playSound(path);
|
||||
}
|
||||
|
||||
function playRandomQuakeSound(){
|
||||
playQuakeSound(quake[Math.floor((Math.random() * quake.length) )]);
|
||||
}
|
||||
|
||||
var quake = [
|
||||
"dominating.wav"
|
||||
,"doublekill.wav"
|
||||
,"doublekill2.wav"
|
||||
,"eagleeye.wav"
|
||||
,"firstblood.wav"
|
||||
,"firstblood2.wav"
|
||||
,"firstblood3.wav"
|
||||
,"flawless.wav"
|
||||
,"godlike.wav"
|
||||
,"hattrick.wav"
|
||||
,"headhunter.wav"
|
||||
,"headshot.wav"
|
||||
,"headshot2.wav"
|
||||
,"headshot3.wav"
|
||||
,"holyshit.wav"
|
||||
,"killingspree.wav"
|
||||
,"knife.wav"
|
||||
,"knife2.wav"
|
||||
,"knife3.wav"
|
||||
,"ludicrouskill.wav"
|
||||
,"megakill.wav"
|
||||
,"monsterkill.wav"
|
||||
,"multikill.wav"
|
||||
,"nade.wav"
|
||||
,"ownage.wav"
|
||||
,"payback.wav"
|
||||
,"prepare.wav"
|
||||
,"prepare2.wav"
|
||||
,"prepare3.wav"
|
||||
,"prepare4.wav"
|
||||
,"rampage.wav"
|
||||
,"suicide.wav"
|
||||
,"suicide2.wav"
|
||||
,"suicide3.wav"
|
||||
,"suicide4.wav"
|
||||
,"teamkiller.wav"
|
||||
,"triplekill.wav"
|
||||
,"ultrakill.wav"
|
||||
,"unstoppable.wav"
|
||||
,"whickedsick.wav"];
|
231
BTCPayServer/wwwroot/crowdfund/services/fireworks.js
Normal file
231
BTCPayServer/wwwroot/crowdfund/services/fireworks.js
Normal file
@ -0,0 +1,231 @@
|
||||
addLoadEvent(function(){
|
||||
var c = document.getElementById("fireworks");
|
||||
var ctx = c.getContext("2d");
|
||||
var cH;
|
||||
var cW;
|
||||
var bgColor = "#FF6138";
|
||||
var animations = [];
|
||||
var circles = [];
|
||||
|
||||
var colorPicker = (function() {
|
||||
var colors = ["#FF6138", "#FFBE53", "#2980B9", "#282741"];
|
||||
var index = 0;
|
||||
function next() {
|
||||
index = index++ < colors.length-1 ? index : 0;
|
||||
return colors[index];
|
||||
}
|
||||
function current() {
|
||||
return colors[index]
|
||||
}
|
||||
return {
|
||||
next: next,
|
||||
current: current
|
||||
}
|
||||
})();
|
||||
|
||||
function removeAnimation(animation) {
|
||||
var index = animations.indexOf(animation);
|
||||
if (index > -1) animations.splice(index, 1);
|
||||
}
|
||||
|
||||
function calcPageFillRadius(x, y) {
|
||||
var l = Math.max(x - 0, cW - x);
|
||||
var h = Math.max(y - 0, cH - y);
|
||||
return Math.sqrt(Math.pow(l, 2) + Math.pow(h, 2));
|
||||
}
|
||||
|
||||
function addClickListeners() {
|
||||
document.addEventListener("touchstart", handleEvent);
|
||||
document.addEventListener("mousedown", handleEvent);
|
||||
};
|
||||
|
||||
function handleEvent(e) {
|
||||
if (e.touches) {
|
||||
e.preventDefault();
|
||||
e = e.touches[0];
|
||||
}
|
||||
var currentColor = colorPicker.current();
|
||||
var nextColor = colorPicker.next();
|
||||
var targetR = calcPageFillRadius(e.pageX, e.pageY);
|
||||
var rippleSize = Math.min(200, (cW * .4));
|
||||
var minCoverDuration = 750;
|
||||
|
||||
var pageFill = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: nextColor
|
||||
});
|
||||
var fillAnimation = anime({
|
||||
targets: pageFill,
|
||||
r: targetR,
|
||||
duration: Math.max(targetR / 2 , minCoverDuration ),
|
||||
easing: "easeOutQuart",
|
||||
complete: function(){
|
||||
bgColor = pageFill.fill;
|
||||
removeAnimation(fillAnimation);
|
||||
}
|
||||
});
|
||||
|
||||
var ripple = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
r: 0,
|
||||
fill: currentColor,
|
||||
stroke: {
|
||||
width: 3,
|
||||
color: currentColor
|
||||
},
|
||||
opacity: 1
|
||||
});
|
||||
var rippleAnimation = anime({
|
||||
targets: ripple,
|
||||
r: rippleSize,
|
||||
opacity: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: 900,
|
||||
complete: removeAnimation
|
||||
});
|
||||
|
||||
var particles = [];
|
||||
for (var i=0; i<32; i++) {
|
||||
var particle = new Circle({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
fill: currentColor,
|
||||
r: anime.random(24, 48)
|
||||
})
|
||||
particles.push(particle);
|
||||
}
|
||||
var particlesAnimation = anime({
|
||||
targets: particles,
|
||||
x: function(particle){
|
||||
return particle.x + anime.random(rippleSize, -rippleSize);
|
||||
},
|
||||
y: function(particle){
|
||||
return particle.y + anime.random(rippleSize * 1.15, -rippleSize * 1.15);
|
||||
},
|
||||
r: 0,
|
||||
easing: "easeOutExpo",
|
||||
duration: anime.random(1000,1300),
|
||||
complete: removeAnimation
|
||||
});
|
||||
animations.push(fillAnimation, rippleAnimation, particlesAnimation);
|
||||
}
|
||||
|
||||
function extend(a, b){
|
||||
for(var key in b) {
|
||||
if(b.hasOwnProperty(key)) {
|
||||
a[key] = b[key];
|
||||
}
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
var Circle = function(opts) {
|
||||
extend(this, opts);
|
||||
}
|
||||
|
||||
Circle.prototype.draw = function() {
|
||||
ctx.globalAlpha = this.opacity || 1;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
|
||||
if (this.stroke) {
|
||||
ctx.strokeStyle = this.stroke.color;
|
||||
ctx.lineWidth = this.stroke.width;
|
||||
ctx.stroke();
|
||||
}
|
||||
if (this.fill) {
|
||||
ctx.fillStyle = this.fill;
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.closePath();
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
var animate = anime({
|
||||
duration: Infinity,
|
||||
update: function() {
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(0, 0, cW, cH);
|
||||
animations.forEach(function(anim) {
|
||||
anim.animatables.forEach(function(animatable) {
|
||||
animatable.target.draw();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var resizeCanvas = function() {
|
||||
cW = window.innerWidth;
|
||||
cH = window.innerHeight;
|
||||
c.width = cW * devicePixelRatio;
|
||||
c.height = cH * devicePixelRatio;
|
||||
ctx.scale(devicePixelRatio, devicePixelRatio);
|
||||
};
|
||||
|
||||
(function init() {
|
||||
resizeCanvas();
|
||||
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
// addClickListeners();
|
||||
|
||||
// handleInactiveUser();
|
||||
})();
|
||||
|
||||
function handleInactiveUser() {
|
||||
var inactive = setTimeout(function(){
|
||||
fauxClick(cW/2, cH/2);
|
||||
}, 2000);
|
||||
|
||||
function clearInactiveTimeout() {
|
||||
clearTimeout(inactive);
|
||||
document.removeEventListener("mousedown", clearInactiveTimeout);
|
||||
document.removeEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
document.addEventListener("mousedown", clearInactiveTimeout);
|
||||
document.addEventListener("touchstart", clearInactiveTimeout);
|
||||
}
|
||||
|
||||
function startFauxClicking() {
|
||||
setTimeout(function(){
|
||||
fauxClick(anime.random( cW * .2, cW * .8), anime.random(cH * .2, cH * .8));
|
||||
startFauxClicking();
|
||||
}, anime.random(200, 900));
|
||||
}
|
||||
|
||||
function fauxClick(x, y) {
|
||||
var fauxClick = new Event("mousedown");
|
||||
fauxClick.pageX = x;
|
||||
fauxClick.pageY = y;
|
||||
document.dispatchEvent(fauxClick);
|
||||
}
|
||||
|
||||
window.fireworks = function(){
|
||||
var fauxClick = new Event("mousedown");
|
||||
fauxClick.pageX =anime.random( 0, cW );
|
||||
fauxClick.pageY = anime.random(0, cH );
|
||||
|
||||
var middleSpaceX = cW * 0.6;
|
||||
var middleSpaceY = cH * 0.6;
|
||||
var middleSpaceX1 = middleSpaceX /2;
|
||||
var middleSpaceX2 = middleSpaceX1 + middleSpaceX;
|
||||
var middleSpaceY1 = middleSpaceY /2;
|
||||
var middleSpaceY2 = middleSpaceY1 + middleSpaceY;
|
||||
while(true){
|
||||
if(fauxClick.pageX > middleSpaceX1 && fauxClick.pageX < middleSpaceX2){
|
||||
fauxClick.pageX =anime.random( 0, cW );
|
||||
continue;
|
||||
}
|
||||
if(fauxClick.pageY > middleSpaceY1 && fauxClick.pageY < middleSpaceY2){
|
||||
fauxClick.pageY =anime.random( 0, cH );
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
handleEvent(fauxClick)
|
||||
};
|
||||
|
||||
|
||||
});
|
50
BTCPayServer/wwwroot/crowdfund/services/listener.js
Normal file
50
BTCPayServer/wwwroot/crowdfund/services/listener.js
Normal file
@ -0,0 +1,50 @@
|
||||
|
||||
var hubListener = function(){
|
||||
|
||||
var connection = new signalR.HubConnectionBuilder().withUrl("/apps/crowdfund/hub").build();
|
||||
|
||||
connection.onclose(function(){
|
||||
eventAggregator.$emit("connection-lost");
|
||||
console.error("Connection was closed. Attempting reconnect in 2s");
|
||||
setTimeout(connect, 2000);
|
||||
});
|
||||
connection.on("PaymentReceived", function(amount, cryptoCode, type){
|
||||
eventAggregator.$emit("payment-received", amount,cryptoCode, type);
|
||||
});
|
||||
connection.on("InvoiceCreated", function(invoiceId){
|
||||
eventAggregator.$emit("invoice-created", invoiceId);
|
||||
});
|
||||
connection.on("InvoiceError", function(error){
|
||||
eventAggregator.$emit("invoice-error", error);
|
||||
});
|
||||
connection.on("InfoUpdated", function(model){
|
||||
eventAggregator.$emit("info-updated", model);
|
||||
});
|
||||
|
||||
function connect(){
|
||||
|
||||
eventAggregator.$emit("connection-pending");
|
||||
connection
|
||||
.start()
|
||||
.then(function(){
|
||||
connection.invoke("ListenToCrowdfundApp", srvModel.appId);
|
||||
|
||||
})
|
||||
.catch(function (err) {
|
||||
eventAggregator.$emit("connection-failed");
|
||||
console.error("Could not connect to backend. Retrying in 2s", err );
|
||||
setTimeout(connect, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
eventAggregator.$on("contribute", function(model){
|
||||
connection.invoke("CreateInvoice", model);
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
connect: connect
|
||||
};
|
||||
}();
|
||||
|
55
BTCPayServer/wwwroot/crowdfund/styles/main.css
Normal file
55
BTCPayServer/wwwroot/crowdfund/styles/main.css
Normal file
@ -0,0 +1,55 @@
|
||||
[v-cloak] > * {
|
||||
display: none
|
||||
}
|
||||
|
||||
[v-cloak]::before {
|
||||
content: "loading…"
|
||||
}
|
||||
|
||||
canvas#fireworks {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.perk-zoom {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.perk-zoom-bg {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.perk-zoom-text {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.perk:hover .perk-zoom {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.perk-badge {
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
position: absolute;
|
||||
left: -15px;
|
||||
top: -15px;
|
||||
padding-top: 5px;
|
||||
}
|
BIN
BTCPayServer/wwwroot/img/icons/icon-128x128.png
Executable file
BIN
BTCPayServer/wwwroot/img/icons/icon-128x128.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-144x144.png
Executable file
BIN
BTCPayServer/wwwroot/img/icons/icon-144x144.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-152x152.png
Executable file
BIN
BTCPayServer/wwwroot/img/icons/icon-152x152.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-192x192.png
Executable file
BIN
BTCPayServer/wwwroot/img/icons/icon-192x192.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user