Compare commits

..

29 Commits

Author SHA1 Message Date
5f15976c02 bump 2019-02-27 18:46:15 +09:00
7f592639c5 Remove URI rewritting and ExternalUri stuff 2019-02-27 18:38:11 +09:00
a98402af12 Making currency switching indicator more obvious with button style (#616) 2019-02-27 13:45:58 +09:00
316ffa91d1 bump 2019-02-26 23:10:58 +09:00
c24953b57e Making hamburger light to see it on dark background (#613) 2019-02-26 14:08:03 +09:00
7a1b1b7e5e Merge branch 'payment-requests' 2019-02-25 17:59:02 +09:00
70f71f64c4 Use internal tags, not order id in the streamer to know if the incoming invoice is for the payment request 2019-02-25 17:56:29 +09:00
5bccd07d7d Make sure the invoiceEvent is from a payment request. 2019-02-25 17:56:29 +09:00
d818baa6d1 Fix crowdfund test 2019-02-25 17:56:29 +09:00
249b8abf03 deduct network from contributions + removed unsued Enabled properties 2019-02-25 17:56:29 +09:00
c134277514 remove creating state from payment requests 2019-02-25 17:56:29 +09:00
f5d366cf7f Fix final bugs 2019-02-25 17:56:29 +09:00
ad25a2ed08 Add payment requests 2019-02-25 17:56:28 +09:00
1e7a2ffe97 Enable/Disable tips and discount. Fix custom amount. (#612) 2019-02-25 15:11:03 +09:00
dd52075ff1 Add backoff delay if fetching exchange rate is failing 2019-02-24 22:00:30 +09:00
0253e42bd5 Do not poll in the invoice page if websocket are working 2019-02-23 15:22:17 +09:00
d99774f8d9 fix tests 2019-02-22 22:52:43 +09:00
d563a2ec89 Fix tests 2019-02-22 22:48:39 +09:00
b4b4523193 Round currency up to significant decimal 2019-02-22 22:15:25 +09:00
fbcb69f447 Do not prevent btcpayserver from starting if using insecure protocol for lightning services 2019-02-22 18:24:27 +09:00
8ae5a9c1f7 Fix old crowdfunding invoices 2019-02-22 17:51:38 +09:00
3ef5bfb6eb bump 2019-02-22 17:30:54 +09:00
4016ded584 Affect orderId to crowdfund app invoices 2019-02-22 17:29:54 +09:00
5b0b4adb1c Fix service link for RTL 2019-02-22 15:46:43 +09:00
f1ec3b0c75 bump 2019-02-22 15:08:45 +09:00
b5d55a2066 Add RTL support 2019-02-22 15:06:52 +09:00
0d2c9fe377 Fix https://github.com/btcpayserver/btcpayserver/issues/585 2019-02-22 13:52:35 +09:00
2c7cc9a796 Fix: invoice Price was not being rounded if no taxIncluded present 2019-02-21 21:58:49 +09:00
2e1d623755 fix https://github.com/btcpayserver/btcpayserver/issues/596 2019-02-21 21:30:30 +09:00
63 changed files with 3198 additions and 235 deletions

View File

@ -177,6 +177,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
user.ModifyStore(s => s.NetworkFeeMode = NetworkFeeMode.Never);
var apps = user.GetController<AppsController>();
var vm = Assert.IsType<CreateAppViewModel>(Assert.IsType<ViewResult>(apps.CreateApp().Result).Model);
vm.Name = "test";

View File

@ -0,0 +1,157 @@
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.Models;
using BTCPayServer.Models.AppViewModels;
using BTCPayServer.Models.PaymentRequestViewModels;
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 PaymentRequestTests
{
public PaymentRequestTests(ITestOutputHelper helper)
{
Logs.Tester = new XUnitLog(helper) {Name = "Tests"};
Logs.LogProvider = new XUnitLogProvider(helper);
}
[Fact]
[Trait("Integration", "Integration")]
public void CanCreateViewUpdateAndDeletePaymentRequest()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var user2 = tester.NewAccount();
user2.GrantAccess();
var paymentRequestController = user.GetController<PaymentRequestController>();
var guestpaymentRequestController = user2.GetController<PaymentRequestController>();
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var id = (Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result).RouteValues.Values.First().ToString());
//permission guard for guests editing
Assert
.IsType<NotFoundResult>(guestpaymentRequestController.EditPaymentRequest(id).Result);
request.Title = "update";
Assert.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(id, request).Result);
Assert.Equal(request.Title, Assert.IsType<ViewPaymentRequestViewModel>( Assert.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model).Title);
Assert.False(string.IsNullOrEmpty(id));
Assert.IsType<ViewPaymentRequestViewModel>(Assert
.IsType<ViewResult>(paymentRequestController.ViewPaymentRequest(id).Result).Model);
//Delete
Assert.IsType<ConfirmModel>(Assert
.IsType<ViewResult>(paymentRequestController.RemovePaymentRequestPrompt(id).Result).Model);
Assert.IsType<RedirectToActionResult>(paymentRequestController.RemovePaymentRequest(id).Result);
Assert
.IsType<NotFoundResult>(paymentRequestController.ViewPaymentRequest(id).Result);
}
}
[Fact]
[Trait("Integration", "Integration")]
public async Task CanPayPaymentRequestWhenPossible()
{
using (var tester = ServerTester.Create())
{
tester.Start();
var user = tester.NewAccount();
user.GrantAccess();
user.RegisterDerivationScheme("BTC");
var paymentRequestController = user.GetController<PaymentRequestController>();
Assert.IsType<NotFoundResult>(await paymentRequestController.PayPaymentRequest(Guid.NewGuid().ToString()));
var request = new UpdatePaymentRequestViewModel()
{
Title = "original juice",
Currency = "BTC",
Amount = 1,
StoreId = user.StoreId,
Description = "description"
};
var response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
var invoiceId = Assert
.IsType<OkObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false)).Value
.ToString();
var actionResult = Assert
.IsType<RedirectToActionResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString()));
Assert.Equal("Checkout", actionResult.ActionName);
Assert.Equal("Invoice", actionResult.ControllerName);
Assert.Contains(actionResult.RouteValues, pair => pair.Key == "Id" && pair.Value.ToString() == invoiceId);
var invoice = user.BitPay.GetInvoice(invoiceId, Facade.Merchant);
Assert.Equal(1, invoice.Price);
request = new UpdatePaymentRequestViewModel()
{
Title = "original juice with expiry",
Currency = "BTC",
Amount = 1,
ExpiryDate = DateTime.Today.Subtract( TimeSpan.FromDays(2)),
StoreId = user.StoreId,
Description = "description"
};
response = Assert
.IsType<RedirectToActionResult>(paymentRequestController.EditPaymentRequest(null, request).Result)
.RouteValues.First();
Assert
.IsType<BadRequestObjectResult>(await paymentRequestController.PayPaymentRequest(response.Value.ToString(), false));
}
}
}
}

View File

@ -1997,31 +1997,48 @@ donation:
var invoice1 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000012m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
Currency = "USD",
FullNotifications = true
}, Facade.Merchant);
var invoice2 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000019m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
Currency = "USD"
}, Facade.Merchant);
Assert.Equal(0.000000012m, invoice1.Price);
Assert.Equal(0.000000019m, invoice2.Price);
// Should round up to 1 because 0.000000019 is unsignificant
var invoice3 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.000000019m,
Currency = "USD",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.00000001m, invoice1.Price);
Assert.Equal(0.00000002m, invoice2.Price);
Assert.Equal(1m, invoice3.Price);
// Should not round up at 8 digit because the 9th is insignificant
var invoice4 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 1.000000019m,
Currency = "BTC",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(1.00000002m, invoice4.Price);
// But not if the 9th is insignificant
invoice4 = user.BitPay.CreateInvoice(new Invoice()
{
Price = 0.000000019m,
Currency = "BTC",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.000000019m, invoice4.Price);
var invoice = user.BitPay.CreateInvoice(new Invoice()
{
Price = -0.1m,
Currency = "BTC",
PosData = "posData",
OrderId = "orderId",
ItemDesc = "Some description",
FullNotifications = true
}, Facade.Merchant);
Assert.Equal(0.0m, invoice.Price);

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<Version>1.0.3.56</Version>
<Version>1.0.3.66</Version>
<NoWarn>NU1701,CA1816,CA1308,CA1810,CA2208</NoWarn>
</PropertyGroup>
<PropertyGroup>
@ -33,7 +33,7 @@
<EmbeddedResource Include="Currencies.txt" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.5" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.1.0.9" />
<PackageReference Include="BuildBundlerMinifier" Version="2.7.385" />
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="0.5.3" />
<PackageReference Include="HtmlSanitizer" Version="4.0.199" />
@ -139,7 +139,7 @@
<Content Update="Views\Server\LightningChargeServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SparkServices.cshtml">
<Content Update="Views\Server\LightningWalletServices.cshtml">
<Pack>$(IncludeRazorContentInPack)</Pack>
</Content>
<Content Update="Views\Server\SSHService.cshtml">

View File

@ -109,18 +109,22 @@ namespace BTCPayServer.Configuration
{
if (!LightningConnectionString.TryParse(lightning, true, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.lightning, " + Environment.NewLine +
$"If you have a c-lightning server use: 'type=clightning;server=/root/.lightning/lightning-rpc', " + Environment.NewLine +
$"If you have a lightning charge server: 'type=charge;server=https://charge.example.com;api-token=yourapitoken'" + Environment.NewLine +
$"If you have a lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$" lnd server: 'type=lnd-rest;server=https://lnd:lnd@lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
if (connectionString.IsLegacy)
else
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
if (connectionString.IsLegacy)
{
Logs.Configuration.LogWarning($"Setting {net.CryptoCode}.lightning is a deprecated format, it will work now, but please replace it for future versions with '{connectionString.ToString()}'");
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
InternalLightningByCryptoCode.Add(net.CryptoCode, connectionString);
}
}
@ -131,28 +135,57 @@ namespace BTCPayServer.Configuration
{
if (!LightningConnectionString.TryParse(lightning, false, out var connectionString, out var error))
{
throw new ConfigException($"Invalid setting {code}, " + Environment.NewLine +
Logs.Configuration.LogWarning($"Invalid setting {code}, " + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroon=abf239...;certthumbprint=2abdf302...'" + Environment.NewLine +
$"lnd server: 'type={lndType};server=https://lnd.example.com;macaroonfilepath=/root/.lnd/admin.macaroon;certthumbprint=2abdf302...'" + Environment.NewLine +
error);
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
var instanceType = typeof(T);
ExternalServicesByCryptoCode.Add(net.CryptoCode, (ExternalService)Activator.CreateInstance(instanceType, connectionString));
}
};
externalLnd<ExternalLndGrpc>($"{net.CryptoCode}.external.lnd.grpc", "lnd-grpc");
externalLnd<ExternalLndRest>($"{net.CryptoCode}.external.lnd.rest", "lnd-rest");
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
if (!SparkConnectionString.TryParse(spark, out var connectionString))
var spark = conf.GetOrDefault<string>($"{net.CryptoCode}.external.spark", string.Empty);
if (spark.Length != 0)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'");
if (!SparkConnectionString.TryParse(spark, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.spark, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/spark/btc/;cookiefile=/etc/clightning_bitcoin_spark/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
}
}
{
var rtl = conf.GetOrDefault<string>($"{net.CryptoCode}.external.rtl", string.Empty);
if (rtl.Length != 0)
{
if (!SparkConnectionString.TryParse(rtl, out var connectionString, out var error))
{
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.rtl, " + Environment.NewLine +
$"Valid example: 'server=https://btcpay.example.com/rtl/btc/;cookiefile=/etc/clightning_bitcoin_rtl/.cookie'" + Environment.NewLine +
$"Error: {error}" + Environment.NewLine +
"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalRTL(connectionString));
}
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalSpark(connectionString));
}
var charge = conf.GetOrDefault<string>($"{net.CryptoCode}.external.charge", string.Empty);
@ -161,14 +194,18 @@ namespace BTCPayServer.Configuration
if (!LightningConnectionString.TryParse(charge, false, out var chargeConnectionString, out var chargeError))
LightningConnectionString.TryParse("type=charge;" + charge, false, out chargeConnectionString, out chargeError);
if(chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
if (chargeConnectionString == null || chargeConnectionString.ConnectionType != LightningConnectionType.Charge)
{
throw new ConfigException($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
Logs.Configuration.LogWarning($"Invalid setting {net.CryptoCode}.external.charge, " + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;api-token=2abdf302...'" + Environment.NewLine +
$"lightning charge server: 'type=charge;server=https://charge.example.com;cookiefilepath=/root/.charge/.cookie'" + Environment.NewLine +
chargeError ?? string.Empty);
$"Error: {chargeError ?? string.Empty}" + Environment.NewLine +
$"This service will not be exposed through BTCPay Server");
}
else
{
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
ExternalServicesByCryptoCode.Add(net.CryptoCode, new ExternalCharge(chargeConnectionString));
}
}
@ -190,7 +227,6 @@ namespace BTCPayServer.Configuration
PostgresConnectionString = conf.GetOrDefault<string>("postgres", null);
MySQLConnectionString = conf.GetOrDefault<string>("mysql", null);
BundleJsCss = conf.GetOrDefault<bool>("bundlejscss", true);
ExternalUrl = conf.GetOrDefault<Uri>("externalurl", null);
var sshSettings = ParseSSHConfiguration(conf);
if ((!string.IsNullOrEmpty(sshSettings.Password) || !string.IsNullOrEmpty(sshSettings.KeyFile)) && !string.IsNullOrEmpty(sshSettings.Server))
@ -311,11 +347,6 @@ namespace BTCPayServer.Configuration
get;
set;
}
public Uri ExternalUrl
{
get;
set;
}
public bool BundleJsCss
{
get;
@ -327,14 +358,5 @@ namespace BTCPayServer.Configuration
get;
set;
}
internal string GetRootUri()
{
if (ExternalUrl == null)
return null;
UriBuilder builder = new UriBuilder(ExternalUrl);
builder.Path = RootPath;
return builder.ToString();
}
}
}

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalRTL : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
public ExternalRTL(SparkConnectionString connectionString)
{
if (connectionString == null)
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
return await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile);
}
}
}

View File

@ -5,7 +5,12 @@ using System.Threading.Tasks;
namespace BTCPayServer.Configuration.External
{
public class ExternalSpark : ExternalService
public interface IAccessKeyService
{
SparkConnectionString ConnectionString { get; }
Task<string> ExtractAccessKey();
}
public class ExternalSpark : ExternalService, IAccessKeyService
{
public SparkConnectionString ConnectionString { get; }
@ -15,5 +20,19 @@ namespace BTCPayServer.Configuration.External
throw new ArgumentNullException(nameof(connectionString));
ConnectionString = connectionString;
}
public async Task<string> ExtractAccessKey()
{
if (ConnectionString?.CookeFile == null)
throw new FormatException("Invalid connection string");
var cookie = (ConnectionString.CookeFile == "fake"
? "fake:fake:fake" // Hacks for testing
: await System.IO.File.ReadAllTextAsync(ConnectionString.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
return cookie[2];
}
throw new FormatException("Invalid cookiefile format");
}
}
}

View File

@ -10,11 +10,11 @@ namespace BTCPayServer.Configuration
public Uri Server { get; private set; }
public string CookeFile { get; private set; }
public static bool TryParse(string str, out SparkConnectionString result)
public static bool TryParse(string str, out SparkConnectionString result, out string error)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
error = null;
result = null;
var resultTemp = new SparkConnectionString();
foreach(var kv in str.Split(';')
@ -25,15 +25,30 @@ namespace BTCPayServer.Configuration
{
case "server":
if (resultTemp.Server != null)
{
error = "Duplicated server attribute";
return false;
}
if (!Uri.IsWellFormedUriString(kv[1], UriKind.Absolute))
{
error = "Invalid URI";
return false;
}
resultTemp.Server = new Uri(kv[1], UriKind.Absolute);
if(resultTemp.Server.Scheme == "http")
{
error = "Insecure transport protocol (http)";
return false;
}
break;
case "cookiefile":
case "cookiefilepath":
if (resultTemp.CookeFile != null)
{
error = "Duplicated cookiefile attribute";
return false;
}
resultTemp.CookeFile = kv[1];
break;
default:

View File

@ -55,12 +55,16 @@ namespace BTCPayServer.Controllers
" custom: true";
EnableShoppingCart = false;
ShowCustomAmount = true;
ShowDiscount = true;
EnableTips = true;
}
public string Title { get; set; }
public string Currency { get; set; }
public string Template { get; set; }
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public const string BUTTON_TEXT_DEF = "Buy for {0}";
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
@ -89,6 +93,8 @@ namespace BTCPayServer.Controllers
Title = settings.Title,
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
Currency = settings.Currency,
Template = settings.Template,
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
@ -160,6 +166,8 @@ namespace BTCPayServer.Controllers
Title = vm.Title,
EnableShoppingCart = vm.EnableShoppingCart,
ShowCustomAmount = vm.ShowCustomAmount,
ShowDiscount = vm.ShowDiscount,
EnableTips = vm.EnableTips,
Currency = vm.Currency.ToUpperInvariant(),
Template = vm.Template,
ButtonText = vm.ButtonText,

View File

@ -62,6 +62,8 @@ namespace BTCPayServer.Controllers
Step = step.ToString(CultureInfo.InvariantCulture),
EnableShoppingCart = settings.EnableShoppingCart,
ShowCustomAmount = settings.ShowCustomAmount,
ShowDiscount = settings.ShowDiscount,
EnableTips = settings.EnableTips,
CurrencyCode = settings.Currency,
CurrencySymbol = numberFormatInfo.CurrencySymbol,
CurrencyInfo = new ViewPointOfSaleViewModel.CurrencyInfoData()
@ -174,6 +176,7 @@ namespace BTCPayServer.Controllers
{
var invoice = await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = AppService.GetCrowdfundOrderId(appId),
Currency = settings.TargetCurrency,
ItemCode = request.ChoiceKey ?? string.Empty,
ItemDesc = title,

View File

@ -70,6 +70,7 @@ namespace BTCPayServer.Controllers
logs.Write("Creation of invoice starting");
var entity = new InvoiceEntity
{
Version = InvoiceEntity.Lastest_Version,
InvoiceTime = DateTimeOffset.UtcNow
};
@ -102,15 +103,19 @@ namespace BTCPayServer.Controllers
entity.RefundMail = entity.BuyerInformation.BuyerEmail;
}
var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m;
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false);
if (currencyInfo != null)
{
invoice.Price = Math.Round(invoice.Price, currencyInfo.CurrencyDecimalDigits);
invoice.TaxIncluded = Math.Round(invoice.TaxIncluded, currencyInfo.CurrencyDecimalDigits);
int divisibility = currencyInfo.CurrencyDecimalDigits;
invoice.Price = invoice.Price.RoundToSignificant(ref divisibility);
divisibility = currencyInfo.CurrencyDecimalDigits;
invoice.TaxIncluded = taxIncluded.RoundToSignificant(ref divisibility);
}
invoice.Price = Math.Max(0.0m, invoice.Price);
invoice.TaxIncluded = Math.Max(0.0m, invoice.TaxIncluded);
invoice.TaxIncluded = Math.Min(invoice.TaxIncluded, invoice.Price);
invoice.TaxIncluded = Math.Max(0.0m, taxIncluded);
invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price);
entity.ProductInformation = Map<CreateInvoiceRequest, ProductInformation>(invoice);

View File

@ -0,0 +1,319 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Rating;
using BTCPayServer.Security;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Ganss.XSS;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using NBitpayClient;
namespace BTCPayServer.Controllers
{
[Route("payment-requests")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
public class PaymentRequestController : Controller
{
private readonly InvoiceController _InvoiceController;
private readonly UserManager<ApplicationUser> _UserManager;
private readonly StoreRepository _StoreRepository;
private readonly PaymentRequestRepository _PaymentRequestRepository;
private readonly PaymentRequestService _PaymentRequestService;
private readonly EventAggregator _EventAggregator;
private readonly CurrencyNameTable _Currencies;
private readonly HtmlSanitizer _htmlSanitizer;
public PaymentRequestController(
InvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
StoreRepository storeRepository,
PaymentRequestRepository paymentRequestRepository,
PaymentRequestService paymentRequestService,
EventAggregator eventAggregator,
CurrencyNameTable currencies,
HtmlSanitizer htmlSanitizer)
{
_InvoiceController = invoiceController;
_UserManager = userManager;
_StoreRepository = storeRepository;
_PaymentRequestRepository = paymentRequestRepository;
_PaymentRequestService = paymentRequestService;
_EventAggregator = eventAggregator;
_Currencies = currencies;
_htmlSanitizer = htmlSanitizer;
}
[HttpGet]
[Route("")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> GetPaymentRequests(int skip = 0, int count = 50, string statusMessage = null)
{
var result = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
{
UserId = GetUserId(), Skip = skip, Count = count
});
return View(new ListPaymentRequestsViewModel()
{
Skip = skip,
StatusMessage = statusMessage,
Count = count,
Total = result.Total,
Items = result.Items.Select(data => new ViewPaymentRequestViewModel(data)).ToList()
});
}
[HttpGet]
[Route("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, string statusMessage = null)
{
SelectList stores = null;
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
{
return NotFound();
}
stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()), nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
if (!stores.Any())
{
return RedirectToAction("GetPaymentRequests",
new
{
StatusMessage = "Error: You need to create at least one store before creating a payment request"
});
}
return View(new UpdatePaymentRequestViewModel(data)
{
Stores = stores,
StatusMessage = statusMessage
});
}
[HttpPost]
[Route("edit/{id?}")]
public async Task<IActionResult> EditPaymentRequest(string id, UpdatePaymentRequestViewModel viewModel)
{
if (string.IsNullOrEmpty(viewModel.Currency) ||
_Currencies.GetCurrencyData(viewModel.Currency, false) == null)
ModelState.AddModelError(nameof(viewModel.Currency), "Invalid currency");
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null && !string.IsNullOrEmpty(id))
{
return NotFound();
}
if (!ModelState.IsValid)
{
viewModel.Stores = new SelectList(await _StoreRepository.GetStoresByUserId(GetUserId()),
nameof(StoreData.Id),
nameof(StoreData.StoreName), data?.StoreDataId);
return View(viewModel);
}
if (data == null)
{
data = new PaymentRequestData();
}
data.StoreDataId = viewModel.StoreId;
var blob = data.GetBlob();
blob.Title = viewModel.Title;
blob.Email = viewModel.Email;
blob.Description = _htmlSanitizer.Sanitize(viewModel.Description);
blob.Amount = viewModel.Amount;
blob.ExpiryDate = viewModel.ExpiryDate;
blob.Currency = viewModel.Currency;
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
blob.CustomCSSLink = viewModel.CustomCSSLink;
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
data.SetBlob(blob);
data = await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(data);
_EventAggregator.Publish(new PaymentRequestUpdated()
{
Data = data,
PaymentRequestId = data.Id
});
return RedirectToAction("EditPaymentRequest", new {id = data.Id, StatusMessage = "Saved"});
}
[HttpGet]
[Route("{id}/remove")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> RemovePaymentRequestPrompt(string id)
{
var data = await _PaymentRequestRepository.FindPaymentRequest(id, GetUserId());
if (data == null)
{
return NotFound();
}
var blob = data.GetBlob();
return View("Confirm", new ConfirmModel()
{
Title = $"Remove Payment Request",
Description = $"Are you sure to remove access to remove payment request '{blob.Title}' ?",
Action = "Delete"
});
}
[HttpPost]
[Route("{id}/remove")]
[BitpayAPIConstraint(false)]
public async Task<IActionResult> RemovePaymentRequest(string id)
{
var result = await _PaymentRequestRepository.RemovePaymentRequest(id, GetUserId());
if (result)
{
return RedirectToAction("GetPaymentRequests",
new {StatusMessage = "Payment request successfully removed"});
}
else
{
return RedirectToAction("GetPaymentRequests",
new
{
StatusMessage =
"Payment request could not be removed. Any request that has generated invoices cannot be removed."
});
}
}
[HttpGet]
[Route("{id}")]
[AllowAnonymous]
public async Task<IActionResult> ViewPaymentRequest(string id)
{
var result = await _PaymentRequestService.GetPaymentRequest(id, GetUserId());
if (result == null)
{
return NotFound();
}
return View(result);
}
[HttpGet]
[Route("{id}/pay")]
[AllowAnonymous]
public async Task<IActionResult> PayPaymentRequest(string id, bool redirectToInvoice = true,
decimal? amount = null)
{
var result = ((await ViewPaymentRequest(id)) as ViewResult)?.Model as ViewPaymentRequestViewModel;
if (result == null)
{
return NotFound();
}
if (result.AmountDue <= 0)
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new {Id = id});
}
return BadRequest("Payment Request has already been settled.");
}
if (result.ExpiryDate.HasValue && DateTime.Now >= result.ExpiryDate)
{
if (redirectToInvoice)
{
return RedirectToAction("ViewPaymentRequest", new {Id = id});
}
return BadRequest("Payment Request has expired");
}
var statusesAllowedToDisplay = new List<InvoiceStatus>()
{
InvoiceStatus.New
};
var validInvoice = result.Invoices.FirstOrDefault(invoice =>
Enum.TryParse<InvoiceStatus>(invoice.Status, true, out var status) &&
statusesAllowedToDisplay.Contains(status));
if (validInvoice != null)
{
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "Invoice", new {Id = validInvoice.Id});
}
return Ok(validInvoice.Id);
}
if (result.AllowCustomPaymentAmounts && amount != null)
{
var invoiceAmount = result.AmountDue < amount ? result.AmountDue : amount;
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result, invoiceAmount);
}
return await CreateInvoiceForPaymentRequest(id, redirectToInvoice, result);
}
private async Task<IActionResult> CreateInvoiceForPaymentRequest(string id,
bool redirectToInvoice,
ViewPaymentRequestViewModel result,
decimal? amount = null)
{
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
var blob = pr.GetBlob();
var store = pr.StoreData;
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
try
{
var newInvoiceId = (await _InvoiceController.CreateInvoiceCore(new CreateInvoiceRequest()
{
OrderId = $"{PaymentRequestRepository.GetOrderIdForPaymentRequest(id)}",
Currency = blob.Currency,
Price = amount.GetValueOrDefault(result.AmountDue),
FullNotifications = true,
BuyerEmail = result.Email,
RedirectURL = Request.GetDisplayUrl().Replace("/pay", "", StringComparison.InvariantCulture),
}, store, HttpContext.Request.GetAbsoluteRoot(), new List<string>() { PaymentRequestRepository.GetInternalTag(id) })).Data.Id;
if (redirectToInvoice)
{
return RedirectToAction("Checkout", "Invoice", new {Id = newInvoiceId});
}
return Ok(newInvoiceId);
}
catch (BitpayHttpException e)
{
return BadRequest(e.Message);
}
}
private string GetUserId()
{
return _UserManager.GetUserId(User);
}
}
}

View File

@ -27,6 +27,7 @@ using Renci.SshNet;
using BTCPayServer.Logging;
using BTCPayServer.Lightning;
using BTCPayServer.Configuration.External;
using System.Runtime.CompilerServices;
namespace BTCPayServer.Controllers
{
@ -474,7 +475,17 @@ namespace BTCPayServer.Controllers
{
Crypto = cryptoCode,
Type = "Spark server",
Action = nameof(SparkServices),
Action = nameof(SparkService),
Index = i++,
});
}
foreach (var rtlService in _Options.ExternalServicesByCryptoCode.GetServices<ExternalRTL>(cryptoCode))
{
result.LNDServices.Add(new ServicesViewModel.LNDServiceViewModel()
{
Crypto = cryptoCode,
Type = "Ride the Lightning server (RTL)",
Action = nameof(RTLService),
Index = i++,
});
}
@ -548,37 +559,41 @@ namespace BTCPayServer.Controllers
}
[Route("server/services/spark/{cryptoCode}/{index}")]
public async Task<IActionResult> SparkServices(string cryptoCode, int index, bool showQR = false)
public async Task<IActionResult> SparkService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalSpark>(cryptoCode, showQR, "Spark Wallet");
}
[Route("server/services/rtl/{cryptoCode}/{index}")]
public async Task<IActionResult> RTLService(string cryptoCode, int index, bool showQR = false)
{
return await LightningWalletServicesCore<ExternalRTL>(cryptoCode, showQR, "Ride the Lightning Wallet");
}
private async Task<IActionResult> LightningWalletServicesCore<T>(string cryptoCode, bool showQR, string walletName) where T : ExternalService, IAccessKeyService
{
if (!_dashBoard.IsFullySynched(cryptoCode, out var unusud))
{
StatusMessage = $"Error: {cryptoCode} is not fully synched";
return RedirectToAction(nameof(Services));
}
var spark = _Options.ExternalServicesByCryptoCode.GetServices<ExternalSpark>(cryptoCode).Select(c => c.ConnectionString).FirstOrDefault();
if (spark == null)
var external = _Options.ExternalServicesByCryptoCode.GetServices<T>(cryptoCode).Where(c => c?.ConnectionString?.Server != null).FirstOrDefault();
if (external == null)
{
return NotFound();
}
SparkServicesViewModel vm = new SparkServicesViewModel();
LightningWalletServices vm = new LightningWalletServices();
vm.ShowQR = showQR;
vm.WalletName = walletName;
try
{
var cookie = (spark.CookeFile == "fake"
? "fake:fake:fake" // If we are testing, it should not crash
: await System.IO.File.ReadAllTextAsync(spark.CookeFile)).Split(':');
if (cookie.Length >= 3)
{
vm.SparkLink = $"{spark.Server.AbsoluteUri}?access-key={cookie[2]}";
}
vm.ServiceLink = $"{external.ConnectionString.Server.AbsoluteUri}?access-key={await external.ExtractAccessKey()}";
}
catch (Exception ex)
{
StatusMessage = $"Error: {ex.Message}";
return RedirectToAction(nameof(Services));
}
return View(vm);
return View("LightningWalletServices", vm);
}
[Route("server/services/lnd/{cryptoCode}/{index}")]

View File

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using BTCPayServer.Models;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.EntityFrameworkCore.Infrastructure.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
@ -54,6 +55,11 @@ namespace BTCPayServer.Data
{
get; set;
}
public DbSet<PaymentRequestData> PaymentRequests
{
get; set;
}
public DbSet<StoreData> Stores
{
@ -204,6 +210,15 @@ namespace BTCPayServer.Data
o.UniqueId
#pragma warning restore CS0618
});
builder.Entity<PaymentRequestData>()
.HasOne(o => o.StoreData)
.WithMany(i => i.PaymentRequests)
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<PaymentRequestData>()
.HasIndex(o => o.Status);
}
}
}

View File

@ -21,6 +21,7 @@ using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.CoinSwitch;
using BTCPayServer.Security;
using BTCPayServer.Rating;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Mails;
namespace BTCPayServer.Data
@ -41,6 +42,11 @@ namespace BTCPayServer.Data
{
get; set;
}
public List<PaymentRequestData> PaymentRequests
{
get; set;
}
public List<InvoiceData> Invoices { get; set; }

View File

@ -61,6 +61,23 @@ namespace BTCPayServer
}
return value;
}
public static decimal RoundToSignificant(this decimal value, ref int divisibility)
{
if (value != 0m)
{
while (true)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
}
return value;
}
public static PaymentMethodId GetpaymentMethodId(this InvoiceCryptoInfo info)
{
return new PaymentMethodId(info.CryptoCode, Enum.Parse<PaymentTypes>(info.PaymentType));

View File

@ -60,7 +60,7 @@ namespace BTCPayServer.HostedServices
_Subscriptions.Add(_EventAggregator.Subscribe<T>(e => _Events.Writer.TryWrite(e)));
}
public Task StartAsync(CancellationToken cancellationToken)
public virtual Task StartAsync(CancellationToken cancellationToken)
{
_Subscriptions = new List<IEventAggregatorSubscription>();
SubscibeToEvents();
@ -70,7 +70,7 @@ namespace BTCPayServer.HostedServices
}
Task _ProcessingEvents = Task.CompletedTask;
public async Task StopAsync(CancellationToken cancellationToken)
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
_Subscriptions?.ForEach(subscription => subscription.Dispose());
_Cts?.Cancel();

View File

@ -38,9 +38,11 @@ using BTCPayServer.Logging;
using BTCPayServer.HostedServices;
using Meziantou.AspNetCore.BundleTagHelpers;
using System.Security.Claims;
using BTCPayServer.PaymentRequest;
using BTCPayServer.Payments.Changelly;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Security;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using NBXplorer.DerivationStrategy;
using NicolasDorier.RateLimits;
@ -74,6 +76,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<BTCPayServerEnvironment>();
services.TryAddSingleton<TokenRepository>();
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<PaymentRequestService>();
services.TryAddSingleton<CoinAverageSettings>();
services.TryAddSingleton<ApplicationDbContextFactory>(o =>
{
@ -150,6 +153,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<LanguageService>();
services.TryAddSingleton<NBXplorerDashboard>();
services.TryAddSingleton<StoreRepository>();
services.TryAddSingleton<PaymentRequestRepository>();
services.TryAddSingleton<BTCPayWalletProvider>();
services.TryAddSingleton<CurrencyNameTable>();
services.TryAddSingleton<IFeeProviderFactory>(o => new NBXplorerFeeProviderFactory(o.GetRequiredService<ExplorerClientProvider>())
@ -184,7 +188,7 @@ namespace BTCPayServer.Hosting
services.AddSingleton<IHostedService, RatesHostedService>();
services.AddSingleton<IHostedService, BackgroundJobSchedulerHostedService>();
services.AddSingleton<IHostedService, AppHubStreamer>();
services.AddSingleton<IHostedService, PaymentRequestStreamer>();
services.AddSingleton<IBackgroundJobClient, BackgroundJobClient>();
services.AddTransient<IConfigureOptions<MvcOptions>, BTCPayClaimsFilter>();
@ -203,6 +207,7 @@ namespace BTCPayServer.Hosting
services.AddTransient<AccessTokenController>();
services.AddTransient<InvoiceController>();
services.AddTransient<AppsPublicController>();
services.AddTransient<PaymentRequestController>();
// Add application services.
services.AddSingleton<EmailSenderFactory>();
// bundling

View File

@ -32,8 +32,6 @@ namespace BTCPayServer.Hosting
public async Task Invoke(HttpContext httpContext)
{
RewriteHostIfNeeded(httpContext);
try
{
var bitpayAuth = GetBitpayAuth(httpContext, out bool isBitpayAuth);
@ -125,83 +123,6 @@ namespace BTCPayServer.Hosting
return false;
}
private void RewriteHostIfNeeded(HttpContext httpContext)
{
string reverseProxyScheme = null;
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Proto", out StringValues proto))
{
var scheme = proto.SingleOrDefault();
if (scheme != null)
{
reverseProxyScheme = scheme;
}
}
ushort? reverseProxyPort = null;
if (httpContext.Request.Headers.TryGetValue("X-Forwarded-Port", out StringValues port))
{
var portString = port.SingleOrDefault();
if (portString != null && ushort.TryParse(portString, out ushort pp))
{
reverseProxyPort = pp;
}
}
// Make sure that code executing after this point think that the external url has been hit.
if (_Options.ExternalUrl != null)
{
if (reverseProxyScheme != null && _Options.ExternalUrl.Scheme != reverseProxyScheme)
{
if (reverseProxyScheme == "http" && _Options.ExternalUrl.Scheme == "https")
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use scheme '{_Options.ExternalUrl.Scheme}' externally, but the reverse proxy uses scheme '{reverseProxyScheme}' (X-Forwarded-Port), forcing ExternalUrl");
}
httpContext.Request.Scheme = _Options.ExternalUrl.Scheme;
if (_Options.ExternalUrl.IsDefaultPort)
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host);
else
{
if (reverseProxyPort != null && _Options.ExternalUrl.Port != reverseProxyPort.Value)
{
Logs.PayServer.LogWarning($"BTCPay ExternalUrl setting expected to use port '{_Options.ExternalUrl.Port}' externally, but the reverse proxy uses port '{reverseProxyPort.Value}'");
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, reverseProxyPort.Value);
}
else
{
httpContext.Request.Host = new HostString(_Options.ExternalUrl.Host, _Options.ExternalUrl.Port);
}
}
}
// NGINX pass X-Forwarded-Proto and X-Forwarded-Port, so let's use that to have better guess of the real domain
else
{
ushort? p = null;
if (reverseProxyScheme != null)
{
httpContext.Request.Scheme = reverseProxyScheme;
if (reverseProxyScheme == "http")
p = 80;
if (reverseProxyScheme == "https")
p = 443;
}
if (reverseProxyPort != null)
{
p = reverseProxyPort.Value;
}
if (p.HasValue)
{
bool isDefault = httpContext.Request.Scheme == "http" && p.Value == 80;
isDefault |= httpContext.Request.Scheme == "https" && p.Value == 443;
if (isDefault)
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host);
else
httpContext.Request.Host = new HostString(httpContext.Request.Host.Host, p.Value);
}
}
}
private static async Task HandleBitpayHttpException(HttpContext httpContext, BitpayHttpException ex)
{
httpContext.Response.StatusCode = ex.StatusCode;

View File

@ -34,6 +34,7 @@ using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Net;
using BTCPayServer.PaymentRequest;
using Meziantou.AspNetCore.BundleTagHelpers;
using BTCPayServer.Security;
using BTCPayServer.Services.Apps;
@ -165,6 +166,7 @@ namespace BTCPayServer.Hosting
app.UseSignalR(route =>
{
route.MapHub<AppHub>("/apps/hub");
route.MapHub<PaymentRequestHub>("/payment-requests/hub");
});
app.UseWebSockets();
app.UseStatusCodePages();

View File

@ -0,0 +1,606 @@
// <auto-generated />
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20190121133309_AddPaymentRequests")]
partial class AddPaymentRequests
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.4-rtm-31024");
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.Property<string>("Address")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset?>("CreatedTime");
b.Property<string>("InvoiceDataId");
b.HasKey("Address");
b.HasIndex("InvoiceDataId");
b.ToTable("AddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd()
.HasMaxLength(50);
b.Property<string>("StoreId")
.HasMaxLength(50);
b.HasKey("Id");
b.HasIndex("StoreId");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AppType");
b.Property<DateTimeOffset>("Created");
b.Property<string>("Name");
b.Property<string>("Settings");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Apps");
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("Address");
b.Property<DateTimeOffset>("Assigned");
b.Property<string>("CryptoCode");
b.Property<DateTimeOffset?>("UnAssigned");
b.HasKey("InvoiceDataId", "Address");
b.ToTable("HistoricalAddressInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<DateTimeOffset>("Created");
b.Property<string>("CustomerEmail");
b.Property<string>("ExceptionStatus");
b.Property<string>("ItemCode");
b.Property<string>("OrderId");
b.Property<string>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("StoreDataId");
b.ToTable("Invoices");
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.Property<string>("InvoiceDataId");
b.Property<string>("UniqueId");
b.Property<string>("Message");
b.Property<DateTimeOffset>("Timestamp");
b.HasKey("InvoiceDataId", "UniqueId");
b.ToTable("InvoiceEvents");
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<DateTimeOffset>("PairingTime");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("SIN");
b.HasIndex("StoreDataId");
b.ToTable("PairedSINData");
});
modelBuilder.Entity("BTCPayServer.Data.PairingCodeData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("DateCreated");
b.Property<DateTimeOffset>("Expiration");
b.Property<string>("Facade");
b.Property<string>("Label");
b.Property<string>("SIN");
b.Property<string>("StoreDataId");
b.Property<string>("TokenValue");
b.HasKey("Id");
b.ToTable("PairingCodes");
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<bool>("Accounted");
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("Payments");
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.Property<string>("Id");
b.HasKey("Id");
b.ToTable("PendingInvoices");
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<string>("InvoiceDataId");
b.HasKey("Id");
b.HasIndex("InvoiceDataId");
b.ToTable("RefundAddresses");
});
modelBuilder.Entity("BTCPayServer.Data.SettingData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Value");
b.HasKey("Id");
b.ToTable("Settings");
});
modelBuilder.Entity("BTCPayServer.Data.StoreData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("DefaultCrypto");
b.Property<string>("DerivationStrategies");
b.Property<string>("DerivationStrategy");
b.Property<int>("SpeedPolicy");
b.Property<byte[]>("StoreBlob");
b.Property<byte[]>("StoreCertificate");
b.Property<string>("StoreName");
b.Property<string>("StoreWebsite");
b.HasKey("Id");
b.ToTable("Stores");
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.Property<string>("ApplicationUserId");
b.Property<string>("StoreDataId");
b.Property<string>("Role");
b.HasKey("ApplicationUserId", "StoreDataId");
b.HasIndex("StoreDataId");
b.ToTable("UserStore");
});
modelBuilder.Entity("BTCPayServer.Models.ApplicationUser", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<int>("AccessFailedCount");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Email")
.HasMaxLength(256);
b.Property<bool>("EmailConfirmed");
b.Property<bool>("LockoutEnabled");
b.Property<DateTimeOffset?>("LockoutEnd");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256);
b.Property<string>("NormalizedUserName")
.HasMaxLength(256);
b.Property<string>("PasswordHash");
b.Property<string>("PhoneNumber");
b.Property<bool>("PhoneNumberConfirmed");
b.Property<bool>("RequiresEmailConfirmation");
b.Property<string>("SecurityStamp");
b.Property<bool>("TwoFactorEnabled");
b.Property<string>("UserName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasName("UserNameIndex");
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<int>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("Status");
b.HasIndex("StoreDataId");
b.ToTable("PaymentRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken();
b.Property<string>("Name")
.HasMaxLength(256);
b.Property<string>("NormalizedName")
.HasMaxLength(256);
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasName("RoleNameIndex");
b.ToTable("AspNetRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("RoleId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("ClaimType");
b.Property<string>("ClaimValue");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider");
b.Property<string>("ProviderKey");
b.Property<string>("ProviderDisplayName");
b.Property<string>("UserId")
.IsRequired();
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("RoleId");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId");
b.Property<string>("LoginProvider");
b.Property<string>("Name");
b.Property<string>("Value");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens");
});
modelBuilder.Entity("BTCPayServer.Data.AddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("AddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.APIKeyData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("APIKeys")
.HasForeignKey("StoreId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.AppData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Apps")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.HistoricalAddressInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("HistoricalAddressInvoices")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("Invoices")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.InvoiceEventData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Events")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PairedSINData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PairedSINs")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PaymentData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("Payments")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.PendingInvoiceData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("PendingInvoices")
.HasForeignKey("Id")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.RefundAddressesData", b =>
{
b.HasOne("BTCPayServer.Data.InvoiceData", "InvoiceData")
.WithMany("RefundAddresses")
.HasForeignKey("InvoiceDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Data.UserStore", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser", "ApplicationUser")
.WithMany("UserStores")
.HasForeignKey("ApplicationUserId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("UserStores")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PaymentRequests")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.HasOne("BTCPayServer.Models.ApplicationUser")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace BTCPayServer.Migrations
{
public partial class AddPaymentRequests : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PaymentRequests",
columns: table => new
{
Id = table.Column<string>(nullable: false),
StoreDataId = table.Column<string>(nullable: true),
Status = table.Column<int>(nullable: false),
Blob = table.Column<byte[]>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PaymentRequests", x => x.Id);
table.ForeignKey(
name: "FK_PaymentRequests_Stores_StoreDataId",
column: x => x.StoreDataId,
principalTable: "Stores",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_PaymentRequests_Status",
table: "PaymentRequests",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_PaymentRequests_StoreDataId",
table: "PaymentRequests",
column: "StoreDataId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PaymentRequests");
}
}
}

View File

@ -328,6 +328,26 @@ namespace BTCPayServer.Migrations
b.ToTable("AspNetUsers");
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.Property<string>("Id")
.ValueGeneratedOnAdd();
b.Property<byte[]>("Blob");
b.Property<int>("Status");
b.Property<string>("StoreDataId");
b.HasKey("Id");
b.HasIndex("Status");
b.HasIndex("StoreDataId");
b.ToTable("PaymentRequests");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
{
b.Property<string>("Id")
@ -528,6 +548,14 @@ namespace BTCPayServer.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("BTCPayServer.Services.PaymentRequests.PaymentRequestData", b =>
{
b.HasOne("BTCPayServer.Data.StoreData", "StoreData")
.WithMany("PaymentRequests")
.HasForeignKey("StoreDataId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole")

View File

@ -17,6 +17,10 @@ namespace BTCPayServer.Models.AppViewModels
public bool EnableShoppingCart { get; set; }
[Display(Name = "User can input custom amount")]
public bool ShowCustomAmount { get; set; }
[Display(Name = "User can input discount in %")]
public bool ShowDiscount { get; set; }
[Display(Name = "Enable tips")]
public bool EnableTips { get; set; }
public string Example1 { get; internal set; }
public string Example2 { get; internal set; }
public string ExampleCallback { get; internal set; }

View File

@ -36,6 +36,8 @@ namespace BTCPayServer.Models.AppViewModels
public bool EnableShoppingCart { get; set; }
public bool ShowCustomAmount { get; set; }
public bool ShowDiscount { get; set; }
public bool EnableTips { get; set; }
public string Step { get; set; }
public string Title { get; set; }
public Item[] Items { get; set; }

View File

@ -72,7 +72,7 @@ namespace BTCPayServer.Models
[JsonProperty(PropertyName = "refundable", DefaultValueHandling = DefaultValueHandling.Ignore)]
public bool Refundable { get; set; }
[JsonProperty(PropertyName = "taxIncluded", DefaultValueHandling = DefaultValueHandling.Ignore)]
public decimal TaxIncluded { get; set; }
public decimal? TaxIncluded { get; set; }
[JsonProperty(PropertyName = "nonce", DefaultValueHandling = DefaultValueHandling.Ignore)]
public long Nonce { get; set; }
[JsonProperty(PropertyName = "guid", DefaultValueHandling = DefaultValueHandling.Ignore)]

View File

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace BTCPayServer.Models.PaymentRequestViewModels
{
public class ListPaymentRequestsViewModel
{
public int Skip { get; set; }
public int Count { get; set; }
public List<ViewPaymentRequestViewModel> Items { get; set; }
public string StatusMessage { get; set; }
public int Total { get; set; }
}
public class UpdatePaymentRequestViewModel
{
public UpdatePaymentRequestViewModel()
{
}
public UpdatePaymentRequestViewModel(PaymentRequestData data)
{
if (data == null)
{
return;
}
Id = data.Id;
StoreId = data.StoreDataId;
var blob = data.GetBlob();
Title = blob.Title;
Amount = blob.Amount;
Currency = blob.Currency;
Description = blob.Description;
ExpiryDate = blob.ExpiryDate;
Email = blob.Email;
CustomCSSLink = blob.CustomCSSLink;
EmbeddedCSS = blob.EmbeddedCSS;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
}
public string Id { get; set; }
[Required] public string StoreId { get; set; }
[Required] public decimal Amount { get; set; }
[Display(Name = "The currency used for payment request. (e.g. BTC, LTC, USD, etc.)")]
public string Currency { get; set; }
[Display(Name = "Expiration Date")]
public DateTime? ExpiryDate { get; set; }
[Required] public string Title { get; set; }
public string Description { get; set; }
public string StatusMessage { get; set; }
public SelectList Stores { get; set; }
[EmailAddress]
public string Email { 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 = "Allow payee to create invoices in their own denomination")]
public bool AllowCustomPaymentAmounts { get; set; }
}
public class ViewPaymentRequestViewModel
{
public ViewPaymentRequestViewModel(PaymentRequestData data)
{
Id = data.Id;
var blob = data.GetBlob();
Title = blob.Title;
Amount = blob.Amount;
Currency = blob.Currency;
Description = blob.Description;
ExpiryDate = blob.ExpiryDate;
Email = blob.Email;
EmbeddedCSS = blob.EmbeddedCSS;
CustomCSSLink = blob.CustomCSSLink;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
switch (data.Status)
{
case PaymentRequestData.PaymentRequestStatus.Pending:
Status = ExpiryDate.HasValue ? $"Expires on {ExpiryDate.Value:g}" : "Pending";
IsPending = true;
break;
case PaymentRequestData.PaymentRequestStatus.Completed:
Status = "Settled";
break;
case PaymentRequestData.PaymentRequestStatus.Expired:
Status = "Expired";
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public bool AllowCustomPaymentAmounts { get; set; }
public string Email { get; set; }
public string Status { get; set; }
public bool IsPending { get; set; }
public decimal AmountCollected { get; set; }
public decimal AmountDue { get; set; }
public string AmountDueFormatted { get; set; }
public decimal Amount { get; set; }
public string Id { get; set; }
public string Currency { get; set; }
public DateTime? ExpiryDate { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public List<PaymentRequestInvoice> Invoices { get; set; } = new List<PaymentRequestInvoice>();
public DateTime LastUpdated { get; set; }
public CurrencyData CurrencyData { get; set; }
public string AmountCollectedFormatted { get; set; }
public string AmountFormatted { get; set; }
public bool AnyPendingInvoice { get; set; }
public class PaymentRequestInvoice
{
public string Id { get; set; }
public DateTime ExpiryDate { get; set; }
public decimal Amount { get; set; }
public string Status { get; set; }
public List<PaymentRequestInvoicePayment> Payments { get; set; }
public string Currency { get; set; }
}
public class PaymentRequestInvoicePayment
{
public string PaymentMethod { get; set; }
public decimal Amount { get; set; }
public string Link { get; set; }
public string Id { get; set; }
}
}
}

View File

@ -5,9 +5,10 @@ using System.Threading.Tasks;
namespace BTCPayServer.Models.ServerViewModels
{
public class SparkServicesViewModel
public class LightningWalletServices
{
public string SparkLink { get; set; }
public string ServiceLink { get; set; }
public bool ShowQR { get; set; }
public string WalletName { get; internal set; }
}
}

View File

@ -0,0 +1,175 @@
using System;
using System.Linq;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Controllers;
using BTCPayServer.Events;
using BTCPayServer.HostedServices;
using BTCPayServer.Logging;
using BTCPayServer.Payments;
using BTCPayServer.Services.PaymentRequests;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using BTCPayServer.Services.Apps;
namespace BTCPayServer.PaymentRequest
{
public class PaymentRequestHub : Hub
{
private readonly PaymentRequestController _PaymentRequestController;
public const string InvoiceCreated = "InvoiceCreated";
public const string PaymentReceived = "PaymentReceived";
public const string InfoUpdated = "InfoUpdated";
public const string InvoiceError = "InvoiceError";
public PaymentRequestHub(PaymentRequestController paymentRequestController)
{
_PaymentRequestController = paymentRequestController;
}
public async Task ListenToPaymentRequest(string paymentRequestId)
{
if (Context.Items.ContainsKey("pr-id"))
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.Items["pr-id"].ToString());
Context.Items.Remove("pr-id");
}
Context.Items.Add("pr-id", paymentRequestId);
await Groups.AddToGroupAsync(Context.ConnectionId, paymentRequestId);
}
public async Task Pay(decimal? amount = null)
{
_PaymentRequestController.ControllerContext.HttpContext = Context.GetHttpContext();
var result =
await _PaymentRequestController.PayPaymentRequest(Context.Items["pr-id"].ToString(), false, amount);
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;
}
}
}
public class PaymentRequestStreamer : EventHostedServiceBase
{
private readonly IHubContext<PaymentRequestHub> _HubContext;
private readonly PaymentRequestRepository _PaymentRequestRepository;
private readonly PaymentRequestService _PaymentRequestService;
public PaymentRequestStreamer(EventAggregator eventAggregator,
IHubContext<PaymentRequestHub> hubContext,
PaymentRequestRepository paymentRequestRepository,
PaymentRequestService paymentRequestService) : base(eventAggregator)
{
_HubContext = hubContext;
_PaymentRequestRepository = paymentRequestRepository;
_PaymentRequestService = paymentRequestService;
}
public override async Task StartAsync(CancellationToken cancellationToken)
{
await base.StartAsync(cancellationToken);
_CheckingPendingPayments = CheckingPendingPayments(cancellationToken)
.ContinueWith(_ => _CheckingPendingPayments = null, TaskScheduler.Default);
}
private async Task CheckingPendingPayments(CancellationToken cancellationToken)
{
Logs.PayServer.LogInformation("Starting payment request expiration watcher");
var (total, items) = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
{
Status = new[] {PaymentRequestData.PaymentRequestStatus.Pending}
}, cancellationToken);
Logs.PayServer.LogInformation($"{total} pending payment requests being checked since last run");
await Task.WhenAll(items.Select(i => _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(i))
.ToArray());
}
Task _CheckingPendingPayments;
public override async Task StopAsync(CancellationToken cancellationToken)
{
await base.StopAsync(cancellationToken);
await (_CheckingPendingPayments ?? Task.CompletedTask);
}
protected override void SubscibeToEvents()
{
Subscribe<InvoiceEvent>();
Subscribe<PaymentRequestUpdated>();
}
protected override async Task ProcessEvent(object evt, CancellationToken cancellationToken)
{
if (evt is InvoiceEvent invoiceEvent)
{
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
{
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
{
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentId);
var data = invoiceEvent.Payment.GetCryptoPaymentData();
await _HubContext.Clients.Group(paymentId).SendCoreAsync(PaymentRequestHub.PaymentReceived,
new object[]
{
data.GetValue(),
invoiceEvent.Payment.GetCryptoCode(),
Enum.GetName(typeof(PaymentTypes),
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
});
}
await InfoUpdated(paymentId);
}
}
else if (evt is PaymentRequestUpdated updated)
{
await InfoUpdated(updated.PaymentRequestId);
var expiry = updated.Data.GetBlob().ExpiryDate;
if (updated.Data.Status == PaymentRequestData.PaymentRequestStatus.Pending &&
expiry.HasValue)
{
QueueExpiryTask(
updated.PaymentRequestId,
expiry.Value,
cancellationToken);
}
}
}
private void QueueExpiryTask(string paymentRequestId, DateTime expiry, CancellationToken cancellationToken)
{
Task.Run(async () =>
{
var delay = expiry - DateTime.Now;
if (delay > TimeSpan.Zero)
await Task.Delay(delay, cancellationToken);
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentRequestId);
}, cancellationToken);
}
private async Task InfoUpdated(string paymentRequestId)
{
var req = await _PaymentRequestService.GetPaymentRequest(paymentRequestId);
if (req != null)
{
await _HubContext.Clients.Group(paymentRequestId)
.SendCoreAsync(PaymentRequestHub.InfoUpdated, new object[] {req});
}
}
}
}

View File

@ -0,0 +1,136 @@
using System;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.PaymentRequests;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.SignalR;
namespace BTCPayServer.PaymentRequest
{
public class PaymentRequestService
{
private readonly PaymentRequestRepository _PaymentRequestRepository;
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly AppService _AppService;
private readonly CurrencyNameTable _currencies;
public PaymentRequestService(
IHubContext<PaymentRequestHub> hubContext,
PaymentRequestRepository paymentRequestRepository,
BTCPayNetworkProvider btcPayNetworkProvider,
AppService appService,
CurrencyNameTable currencies)
{
_PaymentRequestRepository = paymentRequestRepository;
_BtcPayNetworkProvider = btcPayNetworkProvider;
_AppService = appService;
_currencies = currencies;
}
public async Task UpdatePaymentRequestStateIfNeeded(string id)
{
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
await UpdatePaymentRequestStateIfNeeded(pr);
}
public async Task UpdatePaymentRequestStateIfNeeded(PaymentRequestData pr)
{
var blob = pr.GetBlob();
var currentStatus = pr.Status;
if (blob.ExpiryDate.HasValue)
{
if (blob.ExpiryDate.Value <= DateTimeOffset.UtcNow)
currentStatus = PaymentRequestData.PaymentRequestStatus.Expired;
}
else if (pr.Status == PaymentRequestData.PaymentRequestStatus.Pending)
{
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
var amountCollected =
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
if (amountCollected >= blob.Amount)
{
currentStatus = PaymentRequestData.PaymentRequestStatus.Completed;
}
}
if (currentStatus != pr.Status)
{
pr.Status = currentStatus;
await _PaymentRequestRepository.UpdatePaymentRequestStatus(pr.Id, currentStatus);
}
}
public async Task<ViewPaymentRequestViewModel> GetPaymentRequest(string id, string userId = null)
{
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
if (pr == null)
{
return null;
}
var blob = pr.GetBlob();
var rateRules = pr.StoreData.GetStoreBlob().GetRateRules(_BtcPayNetworkProvider);
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
var paymentStats = _AppService.GetCurrentContributionAmountStats(invoices, true);
var amountCollected =
await _AppService.GetCurrentContributionAmount(paymentStats, blob.Currency, rateRules);
var amountDue = blob.Amount - amountCollected;
return new ViewPaymentRequestViewModel(pr)
{
AmountFormatted = _currencies.FormatCurrency(blob.Amount, blob.Currency),
AmountCollected = amountCollected,
AmountCollectedFormatted = _currencies.FormatCurrency(amountCollected, blob.Currency),
AmountDue = amountDue,
AmountDueFormatted = _currencies.FormatCurrency(amountDue, blob.Currency),
CurrencyData = _currencies.GetCurrencyData(blob.Currency, true),
LastUpdated = DateTime.Now,
AnyPendingInvoice = invoices.Any(entity => entity.Status == InvoiceStatus.New),
Invoices = invoices.Select(entity => new ViewPaymentRequestViewModel.PaymentRequestInvoice()
{
Id = entity.Id,
Amount = entity.ProductInformation.Price,
Currency = entity.ProductInformation.Currency,
ExpiryDate = entity.ExpirationTime.DateTime,
Status = entity.GetInvoiceState().ToString(),
Payments = entity.GetPayments().Select(paymentEntity =>
{
var paymentNetwork = _BtcPayNetworkProvider.GetNetwork(paymentEntity.GetCryptoCode());
var paymentData = paymentEntity.GetCryptoPaymentData();
string link = null;
string txId = null;
switch (paymentData)
{
case Payments.Bitcoin.BitcoinLikePaymentData onChainPaymentData:
txId = onChainPaymentData.Outpoint.Hash.ToString();
link = string.Format(CultureInfo.InvariantCulture, paymentNetwork.BlockExplorerLink,
txId);
break;
case LightningLikePaymentData lightningLikePaymentData:
txId = lightningLikePaymentData.BOLT11;
break;
}
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment()
{
Amount = paymentData.GetValue(),
PaymentMethod = paymentEntity.GetPaymentMethodId().ToString(),
Link = link,
Id = txId
};
}).ToList()
}).ToList()
};
}
}
}

View File

@ -47,7 +47,7 @@ namespace BTCPayServer.Services.Apps
{
if (evt is InvoiceEvent invoiceEvent)
{
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice.InternalTags))
foreach (var appId in AppService.GetAppInternalTags(invoiceEvent.Invoice))
{
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
{

View File

@ -167,11 +167,9 @@ namespace BTCPayServer.Services.Apps
public static string GetCrowdfundOrderId(string appId) => $"crowdfund-app_{appId}";
public static string GetAppInternalTag(string appId) => $"APP#{appId}";
public static string[] GetAppInternalTags(IEnumerable<string> tags)
public static string[] GetAppInternalTags(InvoiceEntity invoice)
{
return tags == null ? Array.Empty<string>() : tags
.Where(t => t.StartsWith("APP#", StringComparison.InvariantCulture))
.Select(t => t.Substring("APP#".Length)).ToArray();
return invoice.GetInternalTags("APP#");
}
private async Task<InvoiceEntity[]> GetInvoicesForApp(AppData appData, DateTime? startDate = null)
{
@ -346,7 +344,7 @@ namespace BTCPayServer.Services.Apps
// Else, we just sum the payments
return payments
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue()))
.Select(pay => (Key: pay.GetPaymentMethodId().ToString(), Value: pay.GetCryptoPaymentData().GetValue() - pay.NetworkFee))
.ToArray();
})
.GroupBy(p => p.Key)

View File

@ -114,7 +114,8 @@ namespace BTCPayServer.Services.Invoices
public class InvoiceEntity
{
public const int InternalTagSupport_Version = 1;
public int Version { get; set; } = 1;
public const int Lastest_Version = 1;
public int Version { get; set; }
public string Id
{
get; set;
@ -168,6 +169,13 @@ namespace BTCPayServer.Services.Invoices
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public HashSet<string> InternalTags { get; set; } = new HashSet<string>();
public string[] GetInternalTags(string suffix)
{
return InternalTags == null ? Array.Empty<string>() : InternalTags
.Where(t => t.StartsWith(suffix, StringComparison.InvariantCulture))
.Select(t => t.Substring(suffix.Length)).ToArray();
}
[Obsolete("Use GetDerivationStrategies instead")]
public string DerivationStrategy

View File

@ -0,0 +1,254 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using NBitcoin;
using NBXplorer;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Services.PaymentRequests
{
public class PaymentRequestRepository
{
private readonly ApplicationDbContextFactory _ContextFactory;
private readonly InvoiceRepository _InvoiceRepository;
public PaymentRequestRepository(ApplicationDbContextFactory contextFactory, InvoiceRepository invoiceRepository)
{
_ContextFactory = contextFactory;
_InvoiceRepository = invoiceRepository;
}
public async Task<PaymentRequestData> CreateOrUpdatePaymentRequest(PaymentRequestData entity)
{
using (var context = _ContextFactory.CreateContext())
{
if (string.IsNullOrEmpty(entity.Id))
{
await context.PaymentRequests.AddAsync(entity);
}
else
{
context.PaymentRequests.Update(entity);
}
await context.SaveChangesAsync();
return entity;
}
}
public async Task<PaymentRequestData> FindPaymentRequest(string id, string userId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(id))
{
return null;
}
using (var context = _ContextFactory.CreateContext())
{
return await context.PaymentRequests.Include(x => x.StoreData)
.Where(data =>
string.IsNullOrEmpty(userId) ||
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)))
.SingleOrDefaultAsync(x => x.Id == id, cancellationToken);
}
}
public async Task<bool> IsPaymentRequestAdmin(string paymentRequestId, string userId)
{
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(paymentRequestId))
{
return false;
}
using (var context = _ContextFactory.CreateContext())
{
return await context.PaymentRequests.Include(x => x.StoreData)
.AnyAsync(data =>
data.Id == paymentRequestId &&
(data.StoreData != null && data.StoreData.UserStores.Any(u => u.ApplicationUserId == userId)));
}
}
public async Task UpdatePaymentRequestStatus(string paymentRequestId, PaymentRequestData.PaymentRequestStatus status, CancellationToken cancellationToken = default)
{
using (var context = _ContextFactory.CreateContext())
{
var invoiceData = await context.FindAsync<PaymentRequestData>(paymentRequestId);
if (invoiceData == null)
return;
invoiceData.Status = status;
await context.SaveChangesAsync(cancellationToken);
}
}
public async Task<(int Total, PaymentRequestData[] Items)> FindPaymentRequests(PaymentRequestQuery query, CancellationToken cancellationToken = default)
{
using (var context = _ContextFactory.CreateContext())
{
var queryable = context.PaymentRequests.Include(data => data.StoreData).AsQueryable();
if (!string.IsNullOrEmpty(query.StoreId))
{
queryable = queryable.Where(data =>
data.StoreDataId.Equals(query.StoreId, StringComparison.InvariantCulture));
}
if (query.Status != null && query.Status.Any())
{
queryable = queryable.Where(data =>
query.Status.Contains(data.Status));
}
if (!string.IsNullOrEmpty(query.UserId))
{
queryable = queryable.Where(i =>
i.StoreData != null && i.StoreData.UserStores.Any(u => u.ApplicationUserId == query.UserId));
}
var total = await queryable.CountAsync(cancellationToken);
if (query.Skip.HasValue)
{
queryable = queryable.Skip(query.Skip.Value);
}
if (query.Count.HasValue)
{
queryable = queryable.Take(query.Count.Value);
}
return (total, await queryable.ToArrayAsync(cancellationToken));
}
}
public async Task<bool> RemovePaymentRequest(string id, string userId)
{
using (var context = _ContextFactory.CreateContext())
{
var canDelete = !EnumerableExtensions.Any((await GetInvoicesForPaymentRequest(id)));
if (!canDelete) return false;
var pr = await FindPaymentRequest(id, userId);
if (pr == null)
{
return false;
}
context.PaymentRequests.Remove(pr);
await context.SaveChangesAsync();
return true;
}
}
public async Task<InvoiceEntity[]> GetInvoicesForPaymentRequest(string paymentRequestId,
InvoiceQuery invoiceQuery = null)
{
if (invoiceQuery == null)
{
invoiceQuery = new InvoiceQuery();
}
invoiceQuery.OrderId = new[] {GetOrderIdForPaymentRequest(paymentRequestId)};
return await _InvoiceRepository.GetInvoices(invoiceQuery);
}
public static string GetOrderIdForPaymentRequest(string paymentRequestId)
{
return $"PAY_REQUEST_{paymentRequestId}";
}
public static string GetPaymentRequestIdFromOrderId(string invoiceOrderId)
{
if (string.IsNullOrEmpty(invoiceOrderId) ||
!invoiceOrderId.StartsWith("PAY_REQUEST_", StringComparison.InvariantCulture))
{
return null;
}
return invoiceOrderId.Replace("PAY_REQUEST_", "", StringComparison.InvariantCulture);
}
public static string GetInternalTag(string id)
{
return $"PAYREQ#{id}";
}
public static string[] GetPaymentIdsFromInternalTags(InvoiceEntity invoiceEntity)
{
return invoiceEntity.GetInternalTags("PAYREQ#");
}
}
public class PaymentRequestUpdated
{
public string PaymentRequestId { get; set; }
public PaymentRequestData Data { get; set; }
}
public class PaymentRequestQuery
{
public string StoreId { get; set; }
public PaymentRequestData.PaymentRequestStatus[] Status{ get; set; }
public string UserId { get; set; }
public int? Skip { get; set; }
public int? Count { get; set; }
}
public class PaymentRequestData
{
public string Id { get; set; }
public string StoreDataId { get; set; }
public StoreData StoreData { get; set; }
public PaymentRequestStatus Status { get; set; }
public byte[] Blob { get; set; }
public PaymentRequestBlob GetBlob()
{
var result = Blob == null
? new PaymentRequestBlob()
: JObject.Parse(ZipUtils.Unzip(Blob)).ToObject<PaymentRequestBlob>();
return result;
}
public bool SetBlob(PaymentRequestBlob blob)
{
var original = new Serializer(Network.Main).ToString(GetBlob());
var newBlob = new Serializer(Network.Main).ToString(blob);
if (original == newBlob)
return false;
Blob = ZipUtils.Zip(newBlob);
return true;
}
public class PaymentRequestBlob
{
public decimal Amount { get; set; }
public string Currency { get; set; }
public DateTime? ExpiryDate { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Email { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public bool AllowCustomPaymentAmounts { get; set; }
}
public enum PaymentRequestStatus
{
Pending = 0,
Completed = 1,
Expired = 2
}
}
}

View File

@ -16,6 +16,7 @@ namespace BTCPayServer.Services.Rates
{
public ExchangeRates Latest;
public DateTimeOffset NextRefresh;
public TimeSpan Backoff = TimeSpan.FromSeconds(5.0);
public DateTimeOffset Expiration;
public Exception Exception;
public string ExchangeName;
@ -90,6 +91,7 @@ namespace BTCPayServer.Services.Rates
}
public bool DoNotAutoFetchIfExpired { get; set; }
readonly static TimeSpan MaxBackoff = TimeSpan.FromMinutes(5.0);
public async Task<LatestFetch> UpdateIfNecessary()
{
@ -142,12 +144,15 @@ namespace BTCPayServer.Services.Rates
{
fetch.Latest = previous.Latest;
fetch.Expiration = previous.Expiration;
fetch.Backoff = previous.Backoff * 2;
if (fetch.Backoff > MaxBackoff)
fetch.Backoff = MaxBackoff;
}
else
{
fetch.Expiration = DateTimeOffset.UtcNow;
}
fetch.NextRefresh = DateTimeOffset.UtcNow;
fetch.NextRefresh = DateTimeOffset.UtcNow + fetch.Backoff;
fetch.Exception = ex;
}
_Latest = fetch;

View File

@ -130,19 +130,7 @@ namespace BTCPayServer.Services.Rates
var provider = GetNumberFormatInfo(currency, true);
var currencyData = GetCurrencyData(currency, true);
var divisibility = currencyData.Divisibility;
if (value != 0m)
{
while (true)
{
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
if ((Math.Abs(rounded - value) / value) < 0.001m)
{
value = rounded;
break;
}
divisibility++;
}
}
value = value.RoundToSignificant(ref divisibility);
if (divisibility != provider.CurrencyDecimalDigits)
{
provider = (NumberFormatInfo)provider.Clone();

View File

@ -1,7 +1,7 @@
@using BTCPayServer.Services.Apps
@model ListAppsViewModel
@{
ViewData["Title"] = "Stores";
ViewData["Title"] = "Apps";
}
<section>

View File

@ -15,7 +15,7 @@
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<div class="modal-footer">PRS
<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>

View File

@ -57,6 +57,14 @@
<label asp-for="ShowCustomAmount"></label>
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="ShowDiscount"></label>
<input asp-for="ShowDiscount" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="EnableTips"></label>
<input asp-for="EnableTips" type="checkbox" class="form-check" />
</div>
<div class="form-group">
<label asp-for="ButtonText" class="control-label"></label>*
<input asp-for="ButtonText" class="form-control" />

View File

@ -32,7 +32,7 @@
{
<style>
@Html.Raw(Model.EmbeddedCSS);
</style>
</style>
}
</head>

View File

@ -93,6 +93,8 @@
</div>
</td>
</tr>
@if (Model.ShowDiscount)
{
<tr>
<td colspan="5" class="border-top-0">
<div class="input-group">
@ -106,9 +108,12 @@
</div>
</td>
</tr>
}
</script>
<script id="template-cart-tip" type="text/template">
@if (Model.EnableTips)
{
<tr class="h5">
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
</tr>
@ -135,6 +140,7 @@
</div>
</td>
</tr>
}
</script>
<script id="template-cart-total" type="text/template">
@ -170,18 +176,24 @@
<span class="js-cart-summary-products text-nowrap"></span>
</td>
</tr>
@if (Model.ShowDiscount)
{
<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>
}
@if (Model.EnableTips)
{
<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">

View File

@ -45,11 +45,13 @@
<div class="single-item-order__right">
@if (Model.AvailableCryptos.Count > 1)
{
<div class="payment__currencies cursorPointer" onclick="openPaymentMethodDialog()">
<img v-bind:src="srvModel.cryptoImage" />
<span class="clickable_underline">{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
<span class="clickable_indicator fa fa-angle-right"></span>
<div class="paywithRowRight cursorPointer" onclick="openPaymentMethodDialog()">
<span class="payment__currencies ">
<img v-bind:src="srvModel.cryptoImage" />
<span>{{srvModel.paymentMethodName}} ({{srvModel.cryptoCode}})</span>
<span v-show="srvModel.isLightning">&#9889;</span>
<span class="clickable_indicator fa fa-angle-right"></span>
</span>
</div>
<div id="vexPopupDialog">
<ul class="vexmenu">

View File

@ -1,7 +1,6 @@
@model InvoicesModel
@{
ViewData["Title"] = "Invoices";
var rootUrl = Context.Request.GetAbsoluteRoot();
}
@section HeadScripts {

View File

@ -0,0 +1,122 @@
@using BTCPayServer.Services.PaymentRequests
@model BTCPayServer.Models.PaymentRequestViewModels.UpdatePaymentRequestViewModel
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">@(string.IsNullOrEmpty(Model.Id) ? "Create" : "Edit") Payment Request</h2>
<hr class="primary">
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage"/>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<form method="post">
<input type="hidden" asp-for="Id"/>
<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="Amount" class="control-label"></label>*
<input type="number" step="any" asp-for="Amount" class="form-control"/>
<span asp-validation-for="Amount" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Currency" class="control-label"></label>*
<input placeholder="BTC" asp-for="Currency" class="form-control"/>
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AllowCustomPaymentAmounts"></label>
<input asp-for="AllowCustomPaymentAmounts" type="checkbox" class="form-check"/>
<span asp-validation-for="AllowCustomPaymentAmounts" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="StoreId" class="control-label"></label>
@if (string.IsNullOrEmpty(Model.Id))
{
<select asp-for="StoreId" asp-items="Model.Stores" class="form-control"></select>
}
else
{
<input type="hidden" asp-for="StoreId" value="@Model.StoreId"/>
<input type="text" class="form-control" value="@Model.Stores.Single(item => item.Value == Model.StoreId).Text" readonly/>
}
<span asp-validation-for="StoreId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
<input type="email" asp-for="Email" class="form-control"></input>
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ExpiryDate" class="control-label"></label>
<div class="input-group ">
<input asp-for="ExpiryDate" class="form-control datetime" min="today"/>
<div class="input-group-append only-for-js">
<button class="btn btn-secondary input-group-clear" type="button" title="Clear">
<span class=" fa fa-times"></span>
</button>
</div>
</div>
<span asp-validation-for="ExpiryDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<textarea asp-for="Description" class="form-control richtext"></textarea>
<span asp-validation-for="Description" 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="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">
<button type="submit" class="btn btn-primary">Save</button>
@if (!string.IsNullOrEmpty(Model.Id))
{
<a class="btn btn-secondary" target="_blank" asp-action="ViewPaymentRequest" id="@Model.Id">View</a>
<a class="btn btn-secondary"
target="_blank"
asp-action="ListInvoices"
asp-controller="Invoice"
asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(Model.Id)}")">Invoices</a>
}
<a class="btn btn-secondary" target="_blank" asp-action="GetPaymentRequests">Back to list</a>
</div>
</form>
</div>
</div>
</div>
</section>
@section Scripts {
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.js"></bundle>
<bundle name="wwwroot/bundles/payment-request-admin-bundle.min.css"></bundle>
}

View File

@ -0,0 +1,88 @@
@using BTCPayServer.Services.PaymentRequests
@model BTCPayServer.Models.PaymentRequestViewModels.ListPaymentRequestsViewModel
@{
Layout = "_Layout";
}
<section>
<div class="container">
<div class="row">
<div class="col-lg-12 text-center">
<partial name="_StatusMessage" for="StatusMessage"/>
</div>
</div>
<div class="row">
<div class="col-lg-12 text-center">
<h2 class="section-heading">Payment Requests</h2>
</div>
</div>
<div class="row no-gutter" style="margin-bottom: 5px;">
<div class="col-lg-6">
<a asp-action="EditPaymentRequest" class="btn btn-primary" role="button"><span class="fa fa-plus"></span> Create a new payment request</a>
</div>
</div>
<div class="row">
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Title</th>
<th>Expiry</th>
<th class="text-right">Price</th>
<th class="text-right">Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
{
<tr>
<td>@item.Title</td>
<td>@(item.ExpiryDate?.ToString("g") ?? "No Expiry")</td>
<td class="text-right">@item.Amount @item.Currency</td>
<td class="text-right">@item.Status</td>
<td class="text-right">
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
<span> - </span>
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
<span> - </span>
<a target="_blank" asp-action="ListInvoices" asp-controller="Invoice" asp-route-searchterm="@($"orderid:{PaymentRequestRepository.GetOrderIdForPaymentRequest(item.Id)}")">Invoices</a>
<span> - </span>
<a target="_blank" asp-action="PayPaymentRequest" asp-route-id="@item.Id">Pay</a>
<span> - </span>
<a asp-action="RemovePaymentRequestPrompt" asp-route-id="@item.Id">Remove</a>
</td>
</tr>
}
</tbody>
</table>
<nav aria-label="...">
<ul class="pagination">
<li class="page-item @(Model.Skip == 0 ? "disabled" : null)">
<a class="page-link" tabindex="-1" href="@Url.Action("GetPaymentRequests", new
{
skip = Math.Max(0, Model.Skip - Model.Count),
count = Model.Count,
})">Previous</a>
</li>
<li class="page-item disabled">
<span class="page-link">@(Model.Skip + 1) to @(Model.Skip + Model.Count) of @Model.Total</span>
</li>
<li class="page-item @(Model.Total > (Model.Skip + Model.Count) ? null : "disabled")">
<a class="page-link" href="@Url.Action("GetPaymentRequests", new
{
skip = Model.Skip + Model.Count,
count = Model.Count,
})">Next</a>
</li>
</ul>
</nav>
</div>
</div>
</section>

View File

@ -0,0 +1,163 @@
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
<div class="container">
<div class="row w-100 p-0 m-0" style="height: 100vh">
<div class="mx-auto my-auto w-100">
<div class="card">
<h1 class="card-header">
@Model.Title
<span class="text-muted float-right text-center">@Model.Status</span>
</h1>
<div class="card-body px-0 pt-0">
<div class="row mb-4">
<div class="col-sm-12 col-md-12 col-lg-6 ">
<ul class="w-100 list-group list-group-flush">
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Request amount:</span>
<span class="h2">@Model.AmountFormatted</span>
</div>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Paid so far:</span>
<span class="h2">@Model.AmountCollectedFormatted</span>
</div>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Amount due:</span>
<span class="h2">@Model.AmountDueFormatted</span>
</div>
</li>
</ul>
<div class="w-100 p-2">@Html.Raw(Model.Description)</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="table-responsive">
<table class="table border-top-0 ">
<thead>
<tr>
<th class=" border-top-0" scope="col">Invoice #</th>
<th class=" border-top-0">Price</th>
<th class=" border-top-0">Expiry</th>
<th class=" border-top-0">Status</th>
</tr>
</thead>
<tbody>
@if (Model.Invoices == null && !Model.Invoices.Any())
{
<tr>
<td colspan="4" class="text-center">No payments made yet</td>
</tr>
}
else
{
foreach (var invoice in Model.Invoices)
{
<tr class="bg-light">
<td scope="row">@invoice.Id</td>
<td>@invoice.Amount @invoice.Currency</td>
<td>@invoice.ExpiryDate.ToString("g")</td>
<td>@invoice.Status</td>
</tr>
if (invoice.Payments != null && invoice.Payments.Any())
{
<tr class="bg-light">
<td colspan="4" class=" px-2 py-1 border-top-0">
<div class="table-responsive">
<table class="table table-bordered">
<tr>
<th class="p-1" style="max-width: 300px">Tx Id</th>
<th class="p-1">Payment Method</th>
<th class="p-1">Amount</th>
<th class="p-1">Link</th>
</tr>
@foreach (var payment in invoice.Payments)
{
<tr class="d-flex">
<td class="p-1 m-0 d-print-none d-block" style="max-width: 300px">
<div style="width: 100%; overflow-x: auto; overflow-wrap: initial;">@payment.Id</div>
</td>
<td class="p-1 m-0 d-none d-print-table-cell" style="max-width: 150px;">
@payment.Id
</td>
<td class="p-1">@payment.PaymentMethod</td>
<td class="p-1">@payment.Amount</td>
<td class="p-1 d-print-none">
@if (!string.IsNullOrEmpty(payment.Link))
{
<a :href="@payment.Link" target="_blank">Link</a>
}
</td>
<td class="p-1 d-none d-print-table-cell" style="max-width: 150px;">
@payment.Link
</td>
</tr>
}
</table>
</div>
</td>
</tr>
}
}
}
@if (Model.IsPending)
{
<tr>
<td colspan="4" class="text-center">
@if (Model.AllowCustomPaymentAmounts && !Model.AnyPendingInvoice)
{
<form method="get" asp-action="PayPaymentRequest">
<div class="input-group m-auto" style="max-width: 250px">
<input
class="form-control"
type="number"
name="amount"
value="@Model.AmountDue"
max="@Model.AmountDue"
step="any"
placeholder="Amount"
required>
<div class="input-group-append">
<span class='input-group-text'>@Model.Currency.ToUpper()</span>
<button
class="btn btn-primary"
type="submit">
Pay now
</button>
</div>
</div>
</form>
}
else
{
<a class="btn btn-primary btn-lg d-print-none" asp-action="PayPaymentRequest">
Pay now
</a>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="card-footer text-muted d-flex justify-content-between">
<div >Updated @Model.LastUpdated.ToString("g")</div>
<div >
<span class="text-muted">Powered by </span><a href="https://btcpayserver.org" target="_blank">BTCPay Server</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,213 @@
@model BTCPayServer.Models.PaymentRequestViewModels.ViewPaymentRequestViewModel
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
@{
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.GetRelativePathOrAbsolute(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/payment-request-bundle-1.min.js"></bundle>
<bundle name="wwwroot/bundles/payment-request-bundle-2.min.js"></bundle>
}
<bundle name="wwwroot/bundles/payment-request-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("MinimalPaymentRequest", Model)
}
else
{
<noscript>
@await Html.PartialAsync("MinimalPaymentRequest", Model)
</noscript>
<div class="container" id="app" v-cloak>
<div class="row w-100 p-0 m-0" style="height: 100vh">
<div class="mx-auto my-auto w-100">
<div class="card">
<h1 class="card-header">
{{srvModel.title}}
<span class="text-muted float-right text-center">
<template v-if="settled">Settled</template>
<template v-else>
<template v-if="ended">Request Expired</template>
<template v-else-if="endDiff">Expires in {{endDiff}}</template>
</template>
</span>
</h1>
<div class="card-body px-0 pt-0">
<div class="row mb-4">
<div class="col-sm-12 col-md-12 col-lg-6 ">
<ul class="w-100 list-group list-group-flush">
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Request amount:</span>
<span class="h2">{{srvModel.amountFormatted}}</span>
</div>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Paid so far:</span>
<span class="h2">{{srvModel.amountCollectedFormatted}}</span>
</div>
</li>
<li class="list-group-item">
<div class="d-flex justify-content-between">
<span class="h2 text-muted">Amount due:</span>
<span class="h2">{{srvModel.amountDueFormatted}}</span>
</div>
</li>
</ul>
<div v-html="srvModel.description" class="w-100 p-2"></div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="table-responsive">
<table class="table border-top-0 ">
<thead>
<tr>
<th class=" border-top-0" scope="col">Invoice #</th>
<th class=" border-top-0">Price</th>
<th class=" border-top-0">Expiry</th>
<th class=" border-top-0">Status</th>
</tr>
</thead>
<tbody>
<tr v-if="!srvModel.invoices || srvModel.invoices.length == 0">
<td colspan="4" class="text-center">No payments made yet</td>
</tr>
<template v-else v-for="invoice of srvModel.invoices" :key="invoice.id">
<tr class="bg-light">
<td scope="row">{{invoice.id}}</td>
<td>{{invoice.amount}} {{invoice.currency}}</td>
<td>{{moment(invoice.expiryDate).format('L HH:mm')}}</td>
<td>{{invoice.status}}</td>
</tr>
<tr class="bg-light" v-if="invoice.payments && invoice.payments.length > 0">
<td colspan="4" class=" px-2 py-1 border-top-0">
<div class="table-responsive">
<table class="table table-bordered">
<tr>
<th class="p-1" style="max-width: 300px">Tx Id</th>
<th class="p-1">Payment Method</th>
<th class="p-1">Amount</th>
<th class="p-1">Link</th>
</tr>
<tr v-for="payment of invoice.payments">
<td class="p-1 m-0 d-print-none d-block" style="max-width: 300px">
<div style="width: 100%; overflow-x: auto; overflow-wrap: initial;">{{payment.id}}</div>
</td>
<td class="p-1 m-0 d-none d-print-table-cell" style="max-width: 150px;">
{{payment.id}}
</td>
<td class="p-1">{{formatPaymentMethod(payment.paymentMethod)}}</td>
<td class="p-1">{{payment.amount.noExponents()}}</td>
<td class="p-1 d-print-none">
<a v-if="payment.link" :href="payment.link" target="_blank">Link</a>
</td>
<td class="p-1 d-none d-print-table-cell" style="max-width: 150px;">
{{payment.link}}
</td>
</tr>
</table>
</div>
</td>
</tr>
</template>
<tr v-if="!ended && (srvModel.amountDue) > 0" class="d-print-none">
<td colspan="4" class="text-center">
<template v-if="srvModel.allowCustomPaymentAmounts && !srvModel.anyPendingInvoice">
<form v-on:submit="submitCustomAmountForm">
<div class="input-group m-auto" style="max-width: 250px">
<input
:readonly="!srvModel.allowCustomPaymentAmounts"
class="form-control"
type="number"
v-model="customAmount"
:max="srvModel.amountDue"
step="any"
placeholder="Amount"
required>
<div class="input-group-append">
<span class='input-group-text'>{{currency}}</span>
<button
class="btn btn-primary"
v-bind:class="{ 'btn-disabled': loading}"
:disabled="loading"
type="submit">
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
<span class="sr-only">Loading...</span>
</div>
Pay now
</button>
</div>
</div>
</form>
</template>
<button v-else class="btn btn-primary btn-lg " v-on:click="pay(null)"
:disabled="loading">
<div v-if="loading" class="spinner-grow spinner-grow-sm" role="status">
<span class="sr-only">Loading...</span>
</div>
Pay now
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="card-footer text-muted d-flex justify-content-between">
<div >
<span v-on:click="print" class="btn-link d-print-none" style="cursor: pointer"> <span class="fa fa-print"></span> Print</span>
<span>Updated {{lastUpdated}}</span>
</div>
<div >
<span class="text-muted">Powered by </span><a href="https://btcpayserver.org" target="_blank">BTCPay Server</a>
</div>
</div>
</div>
</div>
</div>
</div>
}
</body>
</html>

View File

@ -1,10 +1,10 @@
@model SparkServicesViewModel
@model LightningWalletServices
@{
ViewData.SetActivePageAndTitle(ServerNavPages.Services);
}
<h4>Spark service</h4>
<h4>@Model.WalletName</h4>
<partial name="_StatusMessage" for="@TempData["StatusMessage"]" />
@if (Model.ShowQR)
@ -30,7 +30,7 @@
<div class="form-group">
<h5>Browser connection</h5>
<p>
<span>You can go to spark from your browser by <a href="@Model.SparkLink" target="_blank">clicking here</a><br /></span>
<span>You can go to @Model.WalletName from your browser by <a href="@Model.ServiceLink" target="_blank">clicking here</a><br /></span>
</p>
</div>
@ -54,7 +54,7 @@
{
<div class="form-group">
<div id="qrCode"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.SparkLink)"></div>
<div id="qrCodeData" data-url="@Html.Raw(Model.ServiceLink)"></div>
</div>
}
</div>
@ -70,7 +70,7 @@
<script type="text/javascript">
new QRCode(document.getElementById("qrCode"),
{
text: "@Html.Raw(Model.SparkLink)",
text: "@Html.Raw(Model.ServiceLink)",
width: 150,
height: 150
});

View File

@ -42,7 +42,7 @@
}
<!-- Navigation -->
<nav class='navbar navbar-expand-lg navbar-light fixed-top @additionalStyle' id="mainNav">
<nav class='navbar navbar-expand-lg navbar-dark fixed-top @additionalStyle' id="mainNav">
<div class="container">
<a class="navbar-brand js-scroll-trigger" href="~/">
<img src="~/img/logo.png" height="45">
@ -66,6 +66,7 @@
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger">Wallets</a></li>
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
<li class="nav-item"><a asp-area="" asp-controller="PaymentRequest" asp-action="GetPaymentRequests" class="nav-link js-scroll-trigger">Payment Requests</a></li>
<li class="nav-item">
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
</li>

View File

@ -124,5 +124,54 @@
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
"wwwroot/crowdfund/**/*.css"
]
},
{
"outputFileName": "wwwroot/bundles/payment-request-admin-bundle.min.js",
"inputFiles": [
"wwwroot/vendor/summernote/summernote-bs4.js",
"wwwroot/vendor/flatpickr/flatpickr.js",
"wwwroot/payment-request-admin/**/*.js"
]
},
{
"outputFileName": "wwwroot/bundles/payment-request-admin-bundle.min.css",
"inputFiles": [
"wwwroot/vendor/summernote/summernote-bs4.css",
"wwwroot/vendor/flatpickr/flatpickr.min.css"
]
},
{
"outputFileName": "wwwroot/bundles/payment-request-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/payment-request/**/*.js"
]
},
{
"outputFileName": "wwwroot/bundles/payment-request-bundle-2.min.js",
"inputFiles": [
"wwwroot/vendor/moment/moment.js"
],
"minify": {
"enabled": false
}
},
{
"outputFileName": "wwwroot/bundles/payment-request-bundle.min.css",
"inputFiles": [
"wwwroot/vendor/font-awesome/css/font-awesome.min.css",
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
"wwwroot/payment-request/**/*.css"
]
}
]

View File

@ -25,6 +25,10 @@ function Cart() {
}
Cart.prototype.setCustomAmount = function(amount) {
if (!srvModel.showCustomAmount) {
return 0;
}
this.customAmount = this.toNumber(amount);
if (this.customAmount > 0) {
@ -36,10 +40,18 @@ Cart.prototype.setCustomAmount = function(amount) {
}
Cart.prototype.getCustomAmount = function() {
if (!srvModel.showCustomAmount) {
return 0;
}
return this.toCents(this.customAmount);
}
Cart.prototype.setTip = function(amount) {
if (!srvModel.enableTips) {
return 0;
}
this.tip = this.toNumber(amount);
if (this.tip > 0) {
@ -51,10 +63,18 @@ Cart.prototype.setTip = function(amount) {
}
Cart.prototype.getTip = function() {
if (!srvModel.enableTips) {
return 0;
}
return this.toCents(this.tip);
}
Cart.prototype.setDiscount = function(amount) {
if (!srvModel.showDiscount) {
return 0;
}
this.discount = this.toNumber(amount);
if (this.discount > 0) {
@ -66,10 +86,18 @@ Cart.prototype.setDiscount = function(amount) {
}
Cart.prototype.getDiscount = function() {
if (!srvModel.showDiscount) {
return 0;
}
return this.toCents(this.discount);
}
Cart.prototype.getDiscountAmount = function(amount) {
if (!srvModel.showDiscount) {
return 0;
}
return this.percentage(amount, this.getDiscount());
}

View File

@ -8223,7 +8223,7 @@ a:hover {
}
.action-button--secondary {
background: #e7e7e7;
background: #e0e0e0;
border: 0;
box-shadow: none;
color: #4A4A4A;
@ -8397,9 +8397,9 @@ strong {
}
.currency-selection {
border-bottom: 1px solid #E9E9E9;
border-bottom: 1px solid #e0e0e0;
position: relative;
padding: 4px 15px;
padding: 4px 10px 4px 15px;
display: flex;
font-weight: 300;
color: #565D6E;
@ -8516,7 +8516,7 @@ strong {
.payment-tabs {
position: relative;
background: #fff;
border-top: 1px solid #E9E9E9;
border-top: 1px solid #e0e0e0;
display: flex;
font-size: 13.5px;
box-shadow: 0px 5px 7px 0px rgba(0, 0, 0, 0.09);
@ -8605,7 +8605,7 @@ strong {
margin-top: 15px;
padding: 15px;
background-color: #ffffff;
border: 1px solid #E9E9E9;
border: 1px solid #e0e0e0;
border-radius: 5px;
width: 100%;
max-height: 300px;
@ -8701,7 +8701,7 @@ strong {
.manual-box.underpaid-expired__refund-pending > .manual-box__amount {
transition: all 250ms cubic-bezier(0.4, 0, 1, 1);
border-top-color: #E9E9E9;
border-top-color: #e0e0e0;
border-top-right-radius: 5px;
border-top-left-radius: 5px;
}
@ -8722,7 +8722,7 @@ strong {
padding-bottom: .7rem;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border: 1px solid #E9E9E9;
border: 1px solid #e0e0e0;
border-bottom: 0;
max-height: 300px;
}
@ -8737,7 +8737,7 @@ strong {
text-align: center;
position: relative;
border-radius: 5px;
border: 1px solid #E9E9E9;
border: 1px solid #e0e0e0;
}
.flipped .manual-box__amount {
@ -8776,8 +8776,8 @@ strong {
height: 10px;
width: 10px;
left: 50%;
border-right: 1px solid #E9E9E9;
border-bottom: 1px solid #E9E9E9;
border-right: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
transform: rotate(45deg);
margin-left: -5px;
bottom: -6px;
@ -8800,8 +8800,8 @@ strong {
height: 10px;
width: 10px;
left: 50%;
border-right: 1px solid #E9E9E9;
border-bottom: 1px solid #E9E9E9;
border-right: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
transform: rotateZ(45deg);
margin-left: -5px;
top: -5px;
@ -8813,7 +8813,7 @@ strong {
.manual-box__address > .flipper {
backface-visibility: hidden;
height: 100%;
border: 1px solid #E9E9E9;
border: 1px solid #e0e0e0;
border-top: 0;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
@ -9155,7 +9155,7 @@ strong {
}
.bp-input {
border: 1px solid #E9E9E9;
border: 1px solid #e0e0e0;
border-radius: 2px;
font-size: 15px;
font-weight: 400;
@ -9482,7 +9482,7 @@ strong {
.payment__scan__qrcode img {
padding: 15px;
background-color: #ffffff;
border: 1px solid #E9E9E9;
border: 1px solid #e0e0e0;
border-radius: 5px;
}
@ -9714,7 +9714,7 @@ strong {
background: #fff;
padding: 30px 15px;
text-align: center;
border: 1px solid #E9E9E9;
border: 1px solid #e0e0e0;
border-radius: 5px;
margin: -5px 0 18px;
font-weight: 300;
@ -11335,7 +11335,7 @@ low-fee-timeline {
text-align: center;
position: relative;
border-radius: 5px;
border: 1px solid #E9E9E9;
border: 1px solid #e0e0e0;
background: #fff;
}
@ -11353,7 +11353,7 @@ low-fee-timeline {
}
.copySectionBox.bottomBorder {
border-bottom: 1px solid #e9e9e9;
border-bottom: 1px solid #e0e0e0;
}
.separatorGem {
@ -11361,8 +11361,8 @@ low-fee-timeline {
height: 10px;
width: 10px;
left: 50%;
border-right: 1px solid #E9E9E9;
border-bottom: 1px solid #E9E9E9;
border-right: 1px solid #e0e0e0;
border-bottom: 1px solid #e0e0e0;
transform: rotateZ(45deg);
margin-left: -5px;
top: -5px;
@ -11374,7 +11374,7 @@ low-fee-timeline {
.checkoutTextbox {
width: 100%;
border: 1px solid #e9e9e9;
border: 1px solid #e0e0e0;
border-radius: 4px;
outline: none;
padding: 8px 4px 8px;
@ -11402,7 +11402,7 @@ low-fee-timeline {
color: #aaa;
height: 16px;
padding: 0px 4px;
border-right: 1px solid #e9e9e9;
border-right: 1px solid #e0e0e0;
}
.inputWithIcon.inputIconBg img {

View File

@ -40,32 +40,35 @@
display: none;
}
.paywithRowRight {
margin-top: -1px;
}
.cursorPointer {
cursor: pointer;
}
.payment__currencies {
font-size: 14px;
font-size: 13px;
border: 1px solid #e0e0e0;
border-radius: 5px;
padding: 6px;
}
.payment__currencies img {
height: 32px;
margin-top: -3px;
height: 24px;
}
.clickable_underline {
border-bottom: 1px dotted #ccc;
font-size: 13px;
}
.payment__currencies:hover .clickable_underline {
border-bottom: 1px dotted black;
}
.payment__currencies:hover .clickable_underline {
border-bottom: 1px dotted black;
}
.payment__currencies:hover {
border: 1px solid #5c6373;
background: #f8f8f8;
}
.clickable_indicator {
margin-right: -10px;
opacity: 0.3;
}
.payment__currencies:hover .clickable_indicator {
opacity: 1;
color: #5c6373;
}

View File

@ -234,6 +234,7 @@ $(document).ready(function () {
// Should connect using webhook ?
// If notification received
var socket = null;
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var loc = window.location, ws_uri;
@ -245,7 +246,7 @@ $(document).ready(function () {
ws_uri += "//" + loc.host;
ws_uri += loc.pathname + "/status/ws?invoiceId=" + srvModel.invoiceId;
try {
var socket = new WebSocket(ws_uri);
socket = new WebSocket(ws_uri);
socket.onmessage = function (e) {
fetchStatus();
};
@ -259,7 +260,9 @@ $(document).ready(function () {
}
var watcher = setInterval(function () {
fetchStatus();
if (socket === null || socket.readyState !== 1) {
fetchStatus();
}
}, 2000);
$(".menu__item").click(function () {

View File

@ -1,8 +1,19 @@
hljs.initHighlightingOnLoad();
$(document).ready(function() {
$(".richtext").summernote();
$(".datetime").flatpickr({
enableTime: true
$(".richtext").summernote({
minHeight: 300
});
$(".datetime").each(function(){
var element = $(this);
var min = element.attr("min");
var max = element.attr("max");
var defaultDate = element.attr("value");
element.flatpickr({
enableTime: true,
minDate: min,
maxDate: max,
defaultDate: defaultDate
});
});
});

View File

@ -190,14 +190,14 @@ addLoadEvent(function (ev) {
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": "";
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.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;

View File

@ -14,3 +14,8 @@
width: 100%;
overflow: hidden;
}
.only-for-js{
display: none;
}

View File

@ -6,4 +6,11 @@
var dateString = localDate.toLocaleDateString() + " " + localDate.toLocaleTimeString();
$(this).text(dateString);
});
$(".input-group-clear").on("click", function(){
$(this).parents(".input-group").find("input").val(null);
});
$(".only-for-js").show();
});

View File

@ -0,0 +1,19 @@
$(document).ready(function() {
$(".richtext").summernote({
minHeight: 300
});
$(".datetime").each(function(){
var element = $(this);
var min = element.attr("min");
var max = element.attr("max");
var defaultDate = element.attr("value");
element.flatpickr({
enableTime: true,
minDate: min,
maxDate: max,
defaultDate: defaultDate
});
});
});

View File

@ -0,0 +1,176 @@
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);
app = new Vue({
el: '#app',
data: function () {
return {
srvModel: window.srvModel,
connectionStatus: "",
endDate: "",
endDateRelativeTime: "",
ended: false,
endDiff: "",
active: true,
lastUpdated: "",
loading: false,
timeoutState: "",
customAmount: null
}
},
computed: {
currency: function () {
return this.srvModel.currency.toUpperCase();
},
settled: function () {
return this.srvModel.amountDue <= 0;
}
},
methods: {
updateComputed: function () {
if (this.srvModel.expiryDate) {
var endDateM = moment(this.srvModel.expiryDate);
this.endDate = endDateM.format('MMMM Do YYYY');
this.endDateRelativeTime = endDateM.fromNow();
this.ended = endDateM.isBefore(moment());
} else {
this.ended = false;
}
if (!this.ended && this.srvModel.expiryDate) {
var mDiffD = moment(this.srvModel.expiryDate).diff(moment(), "days");
var mDiffH = moment(this.srvModel.expiryDate).diff(moment(), "hours");
var mDiffM = moment(this.srvModel.expiryDate).diff(moment(), "minutes");
var mDiffS = moment(this.srvModel.expiryDate).diff(moment(), "seconds");
this.endDiff = mDiffD > 0 ? mDiffD + " days" : mDiffH > 0 ? mDiffH + " hours" : mDiffM > 0 ? mDiffM + " minutes" : mDiffS > 0 ? mDiffS + " seconds" : "";
}
this.lastUpdated = moment(this.srvModel.lastUpdated).calendar();
this.active = !this.ended;
setTimeout(this.updateComputed, 1000);
},
setLoading: function (val) {
this.loading = val;
if (this.timeoutState) {
clearTimeout(this.timeoutState);
}
},
pay: function (amount) {
this.setLoading(true);
var self = this;
self.timeoutState = setTimeout(function () {
self.setLoading(false);
}, 5000);
eventAggregator.$emit("pay", amount);
},
formatPaymentMethod: function (str) {
if (str.endsWith("LightningLike")) {
return str.replace("LightningLike", "Lightning")
}
return str;
},
print:function(){
window.print();
},
submitCustomAmountForm : function(e){
if (e) {
e.preventDefault();
}
if(this.srvModel.allowCustomPaymentAmounts && parseFloat(this.customAmount) < this.srvModel.amountDue){
this.pay(parseFloat(this.customAmount));
}else{
this.pay();
}
}
},
mounted: function () {
this.customAmount = (this.srvModel.amountDue || 0).noExponents();
hubListener.connect();
var self = this;
eventAggregator.$on("invoice-created", function (invoiceId) {
self.setLoading(false);
btcpay.setApiUrlPrefix(window.location.origin);
btcpay.showInvoice(invoiceId);
btcpay.showFrame();
});
eventAggregator.$on("invoice-error", function (error) {
self.setLoading(false);
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";
amount = parseFloat(amount).noExponents();
if (onChain) {
Vue.toasted.show('New payment of ' + amount + " " + cryptoCode + " " + (onChain ? "On Chain" : "LN "), {
iconPack: "fontawesome",
icon: "plus",
duration: 10000
});
} else {
Vue.toasted.show('New payment of ' + amount + " " + cryptoCode + " " + (onChain ? "On Chain" : "LN "), {
iconPack: "fontawesome",
icon: "bolt",
duration: 10000
});
}
});
eventAggregator.$on("info-updated", function (model) {
console.warn("UPDATED", self.srvModel, arguments);
self.srvModel = model;
});
eventAggregator.$on("connection-pending", function () {
self.connectionStatus = "pending";
});
eventAggregator.$on("connection-failed", function () {
self.connectionStatus = "failed";
});
eventAggregator.$on("connection-lost", function () {
self.connectionStatus = "connection lost";
});
this.updateComputed();
}
});
});

View File

@ -0,0 +1,17 @@
Number.prototype.noExponents= function(){
var data= String(this).split(/[eE]/);
if(data.length== 1) return data[0];
var z= '', sign= this<0? '-':'',
str= data[0].replace('.', ''),
mag= Number(data[1])+ 1;
if(mag<0){
z= sign + '0.';
while(mag++) z += '0';
return z + str.replace(/^\-/,'');
}
mag -= str.length;
while(mag--) z += '0';
return str + z;
};

View File

@ -0,0 +1,48 @@
var hubListener = function () {
var connection = new signalR.HubConnectionBuilder().withUrl("/payment-requests/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("ListenToPaymentRequest", srvModel.id);
})
.catch(function (err) {
eventAggregator.$emit("connection-failed");
console.error("Could not connect to backend. Retrying in 2s", err);
setTimeout(connect, 2000);
});
}
eventAggregator.$on("pay", function (amount) {
connection.invoke("Pay", amount);
});
return {
connect: connect
};
}();

View File

@ -0,0 +1,7 @@
[v-cloak] > * {
display: none
}
[v-cloak]::before {
content: "loading…"
}