Use websockets in checkout page to get notified of paid invoices

This commit is contained in:
nicolas.dorier
2017-12-17 19:58:55 +09:00
parent 9d7f5b5b6e
commit aaadda3e0f
5 changed files with 91 additions and 9 deletions

View File

@@ -16,12 +16,14 @@ using System.Security.Claims;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BTCPayServer.Services.Rates; using BTCPayServer.Services.Rates;
using System.Net.WebSockets;
using System.Threading;
using BTCPayServer.Events;
namespace BTCPayServer.Controllers namespace BTCPayServer.Controllers
{ {
public partial class InvoiceController public partial class InvoiceController
{ {
[HttpPost] [HttpPost]
[Route("invoices/{invoiceId}")] [Route("invoices/{invoiceId}")]
public IActionResult Invoice(string invoiceId, string command) public IActionResult Invoice(string invoiceId, string command)
@@ -169,6 +171,73 @@ namespace BTCPayServer.Controllers
return Json(model); return Json(model);
} }
[HttpGet]
[Route("i/{invoiceId}/status/ws")]
public async Task<IActionResult> GetStatusWebSocket(string invoiceId)
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
return NotFound();
var invoice = await _InvoiceRepository.GetInvoice(null, invoiceId);
if (invoice == null || invoice.Status == "complete" || invoice.Status == "invalid" || invoice.Status == "expired")
return NotFound();
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
CompositeDisposable leases = new CompositeDisposable();
try
{
_EventAggregator.Subscribe<Events.InvoiceDataChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId));
_EventAggregator.Subscribe<Events.InvoicePaymentEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId));
_EventAggregator.Subscribe<Events.InvoiceStatusChangedEvent>(async o => await NotifySocket(webSocket, o.InvoiceId, invoiceId));
while (true)
{
var message = await webSocket.ReceiveAsync(DummyBuffer, default(CancellationToken));
if (message.MessageType == WebSocketMessageType.Close)
break;
}
}
finally
{
leases.Dispose();
await CloseSocket(webSocket);
}
return new NoResponse();
}
class NoResponse : IActionResult
{
public Task ExecuteResultAsync(ActionContext context)
{
return Task.CompletedTask;
}
}
ArraySegment<Byte> DummyBuffer = new ArraySegment<Byte>(new Byte[1]);
private async Task NotifySocket(WebSocket webSocket, string invoiceId, string expectedId)
{
if (invoiceId != expectedId || webSocket.State != WebSocketState.Open)
return;
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
try
{
await webSocket.SendAsync(DummyBuffer, WebSocketMessageType.Binary, true, cts.Token);
}
catch { await CloseSocket(webSocket); }
}
private static async Task CloseSocket(WebSocket webSocket)
{
try
{
if (webSocket.State == WebSocketState.Open)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cts.Token);
}
}
finally { webSocket.Dispose(); }
}
[HttpPost] [HttpPost]
[Route("i/{invoiceId}/UpdateCustomer")] [Route("i/{invoiceId}/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data) public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)

View File

@@ -53,7 +53,7 @@ namespace BTCPayServer.Controllers
IFeeProvider _FeeProvider; IFeeProvider _FeeProvider;
private CurrencyNameTable _CurrencyNameTable; private CurrencyNameTable _CurrencyNameTable;
ExplorerClient _Explorer; ExplorerClient _Explorer;
EventAggregator _EventAggregator;
public InvoiceController( public InvoiceController(
Network network, Network network,
InvoiceRepository invoiceRepository, InvoiceRepository invoiceRepository,
@@ -62,6 +62,7 @@ namespace BTCPayServer.Controllers
BTCPayWallet wallet, BTCPayWallet wallet,
IRateProvider rateProvider, IRateProvider rateProvider,
StoreRepository storeRepository, StoreRepository storeRepository,
EventAggregator eventAggregator,
InvoiceWatcherAccessor watcher, InvoiceWatcherAccessor watcher,
ExplorerClient explorerClient, ExplorerClient explorerClient,
IFeeProvider feeProvider) IFeeProvider feeProvider)
@@ -76,6 +77,7 @@ namespace BTCPayServer.Controllers
_Watcher = (watcher ?? throw new ArgumentNullException(nameof(watcher))).Instance; _Watcher = (watcher ?? throw new ArgumentNullException(nameof(watcher))).Instance;
_UserManager = userManager; _UserManager = userManager;
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider)); _FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
_EventAggregator = eventAggregator;
} }
internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15) internal async Task<DataWrapper<InvoiceResponse>> CreateInvoiceCore(Invoice invoice, StoreData store, string serverUrl, double expiryMinutes = 15)

View File

@@ -29,13 +29,11 @@ namespace BTCPayServer.Hosting
{ {
TokenRepository _TokenRepository; TokenRepository _TokenRepository;
RequestDelegate _Next; RequestDelegate _Next;
CallbackController _CallbackController;
BTCPayServerOptions _Options; BTCPayServerOptions _Options;
public BTCPayMiddleware(RequestDelegate next, public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo, TokenRepository tokenRepo,
BTCPayServerOptions options, BTCPayServerOptions options)
CallbackController callbackController)
{ {
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo)); _TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next)); _Next = next ?? throw new ArgumentNullException(nameof(next));
@@ -43,12 +41,9 @@ namespace BTCPayServer.Hosting
} }
bool _Registered;
public async Task Invoke(HttpContext httpContext) public async Task Invoke(HttpContext httpContext)
{ {
RewriteHostIfNeeded(httpContext); RewriteHostIfNeeded(httpContext);
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values); httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault(); var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values); httpContext.Request.Headers.TryGetValue("x-identity", out values);

View File

@@ -144,6 +144,7 @@ namespace BTCPayServer.Hosting
app.UseAuthentication(); app.UseAuthentication();
app.UseHangfireServer(); app.UseHangfireServer();
app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } }); app.UseHangfireDashboard("/hangfire", new DashboardOptions() { Authorization = new[] { new NeedRole(Roles.ServerAdmin) } });
app.UseWebSockets();
app.UseMvc(routes => app.UseMvc(routes =>
{ {
routes.MapRoute( routes.MapRoute(

View File

@@ -191,7 +191,7 @@ function onDataCallback(jsonData) {
checkoutCtrl.srvModel = jsonData; checkoutCtrl.srvModel = jsonData;
} }
var watcher = setInterval(function () { function fetchStatus() {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status"; var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status";
$.ajax({ $.ajax({
url: path, url: path,
@@ -201,6 +201,21 @@ var watcher = setInterval(function () {
}).fail(function (jqXHR, textStatus, errorThrown) { }).fail(function (jqXHR, textStatus, errorThrown) {
}); });
}
var supportsWebSockets = 'WebSocket' in window && window.WebSocket.CLOSING === 2;
if (supportsWebSockets) {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status/ws";
path = path.replace("https://", "wss://");
path = path.replace("http://", "ws://");
var socket = new WebSocket(path);
socket.onmessage = function (e) {
fetchStatus();
};
}
var watcher = setInterval(function () {
fetchStatus();
}, 2000); }, 2000);
$(".menu__item").click(function () { $(".menu__item").click(function () {