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.Threading.Tasks;
using BTCPayServer.Services.Rates;
using System.Net.WebSockets;
using System.Threading;
using BTCPayServer.Events;
namespace BTCPayServer.Controllers
{
public partial class InvoiceController
{
[HttpPost]
[Route("invoices/{invoiceId}")]
public IActionResult Invoice(string invoiceId, string command)
@ -169,6 +171,73 @@ namespace BTCPayServer.Controllers
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]
[Route("i/{invoiceId}/UpdateCustomer")]
public async Task<IActionResult> UpdateCustomer(string invoiceId, [FromBody]UpdateCustomerModel data)

View File

@ -53,7 +53,7 @@ namespace BTCPayServer.Controllers
IFeeProvider _FeeProvider;
private CurrencyNameTable _CurrencyNameTable;
ExplorerClient _Explorer;
EventAggregator _EventAggregator;
public InvoiceController(
Network network,
InvoiceRepository invoiceRepository,
@ -62,6 +62,7 @@ namespace BTCPayServer.Controllers
BTCPayWallet wallet,
IRateProvider rateProvider,
StoreRepository storeRepository,
EventAggregator eventAggregator,
InvoiceWatcherAccessor watcher,
ExplorerClient explorerClient,
IFeeProvider feeProvider)
@ -76,6 +77,7 @@ namespace BTCPayServer.Controllers
_Watcher = (watcher ?? throw new ArgumentNullException(nameof(watcher))).Instance;
_UserManager = userManager;
_FeeProvider = feeProvider ?? throw new ArgumentNullException(nameof(feeProvider));
_EventAggregator = eventAggregator;
}
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;
RequestDelegate _Next;
CallbackController _CallbackController;
BTCPayServerOptions _Options;
public BTCPayMiddleware(RequestDelegate next,
TokenRepository tokenRepo,
BTCPayServerOptions options,
CallbackController callbackController)
BTCPayServerOptions options)
{
_TokenRepository = tokenRepo ?? throw new ArgumentNullException(nameof(tokenRepo));
_Next = next ?? throw new ArgumentNullException(nameof(next));
@ -43,12 +41,9 @@ namespace BTCPayServer.Hosting
}
bool _Registered;
public async Task Invoke(HttpContext httpContext)
{
RewriteHostIfNeeded(httpContext);
httpContext.Request.Headers.TryGetValue("x-signature", out StringValues values);
var sig = values.FirstOrDefault();
httpContext.Request.Headers.TryGetValue("x-identity", out values);

View File

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

View File

@ -191,7 +191,7 @@ function onDataCallback(jsonData) {
checkoutCtrl.srvModel = jsonData;
}
var watcher = setInterval(function () {
function fetchStatus() {
var path = srvModel.serverUrl + "/i/" + srvModel.invoiceId + "/status";
$.ajax({
url: path,
@ -201,6 +201,21 @@ var watcher = setInterval(function () {
}).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);
$(".menu__item").click(function () {