mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Add payment requests
This commit is contained in:
parent
1e7a2ffe97
commit
ad25a2ed08
30 changed files with 2884 additions and 10 deletions
157
BTCPayServer.Tests/PaymentRequestTests.cs
Normal file
157
BTCPayServer.Tests/PaymentRequestTests.cs
Normal 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));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
338
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
338
BTCPayServer/Controllers/PaymentRequestController.cs
Normal file
|
@ -0,0 +1,338 @@
|
|||
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 RateFetcher _RateFetcher;
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
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,
|
||||
RateFetcher rateFetcher,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
PaymentRequestService paymentRequestService,
|
||||
EventAggregator eventAggregator,
|
||||
CurrencyNameTable currencies,
|
||||
HtmlSanitizer htmlSanitizer)
|
||||
{
|
||||
_InvoiceController = invoiceController;
|
||||
_UserManager = userManager;
|
||||
_StoreRepository = storeRepository;
|
||||
_RateFetcher = rateFetcher;
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_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();
|
||||
}
|
||||
|
||||
if (data != null && data.Status != PaymentRequestData.PaymentRequestStatus.Creating)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new
|
||||
{
|
||||
id
|
||||
});
|
||||
}
|
||||
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 (data != null && data.Status != PaymentRequestData.PaymentRequestStatus.Creating)
|
||||
{
|
||||
return RedirectToAction("ViewPaymentRequest", new
|
||||
{
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
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.Status = viewModel.Action == "publish" ? PaymentRequestData.PaymentRequestStatus.Pending : PaymentRequestData.PaymentRequestStatus.Creating;
|
||||
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 Invoice()
|
||||
{
|
||||
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())).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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
606
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.Designer.cs
generated
Normal file
606
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
47
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.cs
Normal file
47
BTCPayServer/Migrations/20190121133309_AddPaymentRequests.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
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 RemovePaymentRequestViewModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Title { 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 string Action { 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.Creating:
|
||||
Status = "Creating";
|
||||
break;
|
||||
case PaymentRequestData.PaymentRequestStatus.Pending:
|
||||
if (ExpiryDate.HasValue)
|
||||
{
|
||||
Status = $"Expires on {ExpiryDate.Value:g}";
|
||||
}
|
||||
else
|
||||
{
|
||||
Status = "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; }
|
||||
}
|
||||
}
|
||||
}
|
175
BTCPayServer/PaymentRequest/PaymentRequestHub.cs
Normal file
175
BTCPayServer/PaymentRequest/PaymentRequestHub.cs
Normal 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 AppService _AppService;
|
||||
private readonly PaymentRequestService _PaymentRequestService;
|
||||
|
||||
|
||||
public PaymentRequestStreamer(EventAggregator eventAggregator,
|
||||
IHubContext<PaymentRequestHub> hubContext,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
AppService appService,
|
||||
PaymentRequestService paymentRequestService) : base(eventAggregator)
|
||||
{
|
||||
_HubContext = hubContext;
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_AppService = appService;
|
||||
_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)
|
||||
{
|
||||
var paymentRequestId =
|
||||
PaymentRequestRepository.GetPaymentRequestIdFromOrderId(invoiceEvent.Invoice.OrderId);
|
||||
|
||||
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
|
||||
{
|
||||
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentRequestId);
|
||||
var data = invoiceEvent.Payment.GetCryptoPaymentData();
|
||||
await _HubContext.Clients.Group(paymentRequestId).SendCoreAsync(PaymentRequestHub.PaymentReceived,
|
||||
new object[]
|
||||
{
|
||||
data.GetValue(),
|
||||
invoiceEvent.Payment.GetCryptoCode(),
|
||||
Enum.GetName(typeof(PaymentTypes),
|
||||
invoiceEvent.Payment.GetPaymentMethodId().PaymentType)
|
||||
});
|
||||
}
|
||||
await InfoUpdated(paymentRequestId);
|
||||
}
|
||||
else if (evt is PaymentRequestUpdated updated)
|
||||
{
|
||||
if (updated.Data.Status != PaymentRequestData.PaymentRequestStatus.Creating)
|
||||
{
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
149
BTCPayServer/PaymentRequest/PaymentRequestService.cs
Normal file
149
BTCPayServer/PaymentRequest/PaymentRequestService.cs
Normal file
|
@ -0,0 +1,149 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
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 IHubContext<PaymentRequestHub> _HubContext;
|
||||
private readonly PaymentRequestRepository _PaymentRequestRepository;
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly AppService _AppService;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
|
||||
public PaymentRequestService(EventAggregator eventAggregator,
|
||||
IHubContext<PaymentRequestHub> hubContext,
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
AppService appService,
|
||||
CurrencyNameTable currencies)
|
||||
{
|
||||
_HubContext = hubContext;
|
||||
_PaymentRequestRepository = paymentRequestRepository;
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_AppService = appService;
|
||||
_currencies = currencies;
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentRequestStateIfNeeded(string id)
|
||||
{
|
||||
var pr = await _PaymentRequestRepository.FindPaymentRequest(id, null);
|
||||
if (pr == null || pr.Status == PaymentRequestData.PaymentRequestStatus.Creating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
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 (pr.Status != PaymentRequestData.PaymentRequestStatus.Creating && 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;
|
||||
}
|
||||
|
||||
if (pr.Status == PaymentRequestData.PaymentRequestStatus.Creating &&
|
||||
!await _PaymentRequestRepository.IsPaymentRequestAdmin(id, userId))
|
||||
{
|
||||
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()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
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 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
|
||||
{
|
||||
Creating = 0,
|
||||
Pending = 1,
|
||||
Completed = 2,
|
||||
Expired = 3
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
@using BTCPayServer.Services.Apps
|
||||
@model ListAppsViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Stores";
|
||||
ViewData["Title"] = "Apps";
|
||||
}
|
||||
|
||||
<section>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
</style>
|
||||
}
|
||||
|
||||
</head>
|
||||
|
|
111
BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml
Normal file
111
BTCPayServer/Views/PaymentRequest/EditPaymentRequest.cshtml
Normal file
|
@ -0,0 +1,111 @@
|
|||
@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" name="action" value="draft">Save for later</button>
|
||||
<button type="submit" class="btn btn-primary" name="action" value="publish">Save & Publish</button>
|
||||
|
||||
<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>
|
||||
}
|
98
BTCPayServer/Views/PaymentRequest/GetPaymentRequests.cshtml
Normal file
98
BTCPayServer/Views/PaymentRequest/GetPaymentRequests.cshtml
Normal file
|
@ -0,0 +1,98 @@
|
|||
@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">
|
||||
@if (item.Status.Equals(nameof(PaymentRequestData.PaymentRequestStatus.Creating), StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
<a asp-action="EditPaymentRequest" asp-route-id="@item.Id">Edit</a>
|
||||
<span> - </span>
|
||||
<a asp-action="ViewPaymentRequest" asp-route-id="@item.Id">View</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<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>
|
||||
}
|
||||
@if (item.Status.Equals(nameof(PaymentRequestData.PaymentRequestStatus.Pending), StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
<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>
|
163
BTCPayServer/Views/PaymentRequest/MinimalPaymentRequest.cshtml
Normal file
163
BTCPayServer/Views/PaymentRequest/MinimalPaymentRequest.cshtml
Normal 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>
|
213
BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml
Normal file
213
BTCPayServer/Views/PaymentRequest/ViewPaymentRequest.cshtml
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -14,3 +14,8 @@
|
|||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.only-for-js{
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
19
BTCPayServer/wwwroot/payment-request-admin/main.js
Normal file
19
BTCPayServer/wwwroot/payment-request-admin/main.js
Normal 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
|
||||
});
|
||||
});
|
||||
|
||||
});
|
176
BTCPayServer/wwwroot/payment-request/app.js
Normal file
176
BTCPayServer/wwwroot/payment-request/app.js
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
17
BTCPayServer/wwwroot/payment-request/helpers/math.js
Normal file
17
BTCPayServer/wwwroot/payment-request/helpers/math.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
Number.prototype.noExponents= function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
str= data[0].replace('.', ''),
|
||||
mag= Number(data[1])+ 1;
|
||||
|
||||
if(mag<0){
|
||||
z= sign + '0.';
|
||||
while(mag++) z += '0';
|
||||
return z + str.replace(/^\-/,'');
|
||||
}
|
||||
mag -= str.length;
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
};
|
48
BTCPayServer/wwwroot/payment-request/services/listener.js
Normal file
48
BTCPayServer/wwwroot/payment-request/services/listener.js
Normal 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
|
||||
};
|
||||
}();
|
||||
|
7
BTCPayServer/wwwroot/payment-request/styles/main.css
Normal file
7
BTCPayServer/wwwroot/payment-request/styles/main.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
[v-cloak] > * {
|
||||
display: none
|
||||
}
|
||||
|
||||
[v-cloak]::before {
|
||||
content: "loading…"
|
||||
}
|
Loading…
Add table
Reference in a new issue