Remove Confirmed state in UI (#3090)

* Remove Confirmed state in UI

Closes #1789.

* Add infobox & improve refund tooltip

* Update BTCPayServer/Views/Invoice/ListInvoices.cshtml

Add @dennisreimann suggestion

Co-authored-by: d11n <mail@dennisreimann.de>

* Add "don't show again" button

Adds a "Don't Show Again" button to the infobox. Also a bugfix that was preventing the new status from showing in the invoice details page.

* Add User blob and move invoice status notice to it

Co-authored-by: d11n <mail@dennisreimann.de>
Co-authored-by: Kukks <evilkukka@gmail.com>
This commit is contained in:
Samuel Adams 2021-11-26 16:13:41 +02:00 committed by GitHub
parent f6afb9a3f0
commit ec68d2a0e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 225 additions and 50 deletions

View file

@ -19,6 +19,8 @@ namespace BTCPayServer.Data
public List<NotificationData> Notifications { get; set; }
public List<UserStore> UserStores { get; set; }
public List<Fido2Credential> Fido2Credentials { get; set; }
public byte[] Blob { get; set; }
public List<IdentityUserRole<string>> UserRoles { get; set; }

View file

@ -0,0 +1,29 @@
using System;
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20211125081400_AddUserBlob")]
public partial class AddUserBlob : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte[]>(
name: "Blob",
table: "AspNetUsers",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Blob",
table: "AspNetUsers");
}
}
}

View file

@ -105,6 +105,9 @@ namespace BTCPayServer.Migrations
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<byte[]>("Blob")
.HasColumnType("BLOB");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");

View file

@ -104,7 +104,7 @@ namespace BTCPayServer.Controllers
StoreLink = Url.Action(nameof(StoresController.PaymentMethods), "Stores", new { storeId = store.Id }),
PaymentRequestLink = Url.Action(nameof(PaymentRequestController.ViewPaymentRequest), "PaymentRequest", new { id = invoice.Metadata.PaymentRequestId }),
Id = invoice.Id,
State = invoiceState.ToString(),
State = invoiceState.Status.ToModernStatus().ToString(),
TransactionSpeed = invoice.SpeedPolicy == SpeedPolicy.HighSpeed ? "high" :
invoice.SpeedPolicy == SpeedPolicy.MediumSpeed ? "medium" :
invoice.SpeedPolicy == SpeedPolicy.LowMediumSpeed ? "low-medium" :
@ -128,7 +128,7 @@ namespace BTCPayServer.Controllers
.Select(c => new Models.StoreViewModels.DeliveryViewModel(c))
.ToList(),
CanMarkInvalid = invoiceState.CanMarkInvalid(),
CanMarkComplete = invoiceState.CanMarkComplete(),
CanMarkSettled = invoiceState.CanMarkComplete(),
};
model.Addresses = invoice.HistoricalAddresses.Select(h =>
new InvoiceDetailsModel.AddressModel
@ -763,7 +763,7 @@ namespace BTCPayServer.Controllers
RedirectUrl = invoice.RedirectURL?.AbsoluteUri ?? string.Empty,
AmountCurrency = _CurrencyNameTable.DisplayFormatCurrency(invoice.Price, invoice.Currency),
CanMarkInvalid = state.CanMarkInvalid(),
CanMarkComplete = state.CanMarkComplete(),
CanMarkSettled = state.CanMarkComplete(),
Details = InvoicePopulatePayments(invoice),
});
}
@ -936,10 +936,10 @@ namespace BTCPayServer.Controllers
await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Invalid);
model.StatusString = new InvoiceState("invalid", "marked").ToString();
}
else if (newState == "complete")
else if (newState == "settled")
{
await _InvoiceRepository.MarkInvoiceStatus(invoiceId, InvoiceStatus.Settled);
model.StatusString = new InvoiceState("complete", "marked").ToString();
model.StatusString = new InvoiceState("settled", "marked").ToString();
}
return Json(model);
@ -999,6 +999,5 @@ namespace BTCPayServer.Controllers
return result;
}
}
}
}

View file

@ -85,6 +85,26 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DisableShowInvoiceStatusChangeHint()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var blob = user.GetBlob();
blob.ShowInvoiceStatusChangeHint = false;
if (user.SetBlob(blob))
{
await _userManager.UpdateAsync(user);
}
return RedirectToAction(nameof(Index));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(IndexViewModel model)

View file

@ -0,0 +1,33 @@
using System.Linq;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Newtonsoft.Json.Linq;
namespace BTCPayServer
{
public static class UserExtensions
{
public static UserBlob GetBlob(this ApplicationUser user)
{
var result = user.Blob == null
? new UserBlob()
: JObject.Parse(ZipUtils.Unzip(user.Blob)).ToObject<UserBlob>();
return result;
}
public static bool SetBlob(this ApplicationUser user, UserBlob blob)
{
var newBytes = InvoiceRepository.ToBytes(blob);
if (user.Blob != null && newBytes.SequenceEqual(user.Blob))
{
return false;
}
user.Blob = newBytes;
return true;
}
}
public class UserBlob
{
public bool ShowInvoiceStatusChangeHint { get; set; }
}
}

View file

@ -163,6 +163,12 @@ namespace BTCPayServer.Hosting
settings.MigratePayoutDestinationId = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.AddInitialUserBlob)
{
await AddInitialUserBlob();
settings.AddInitialUserBlob = true;
await _Settings.UpdateSetting(settings);
}
}
catch (Exception ex)
{
@ -171,6 +177,16 @@ namespace BTCPayServer.Hosting
}
}
private async Task AddInitialUserBlob()
{
await using var ctx = _DBContextFactory.CreateContext();
foreach (var user in await ctx.Users.AsQueryable().ToArrayAsync())
{
user.SetBlob(new UserBlob() { ShowInvoiceStatusChangeHint = true });
}
await ctx.SaveChangesAsync();
}
private async Task MigratePayoutDestinationId()
{
await using var ctx = _DBContextFactory.CreateContext();

View file

@ -129,8 +129,8 @@ namespace BTCPayServer.Models.InvoicingModels
public bool Archived { get; set; }
public bool CanRefund { get; set; }
public bool ShowCheckout { get; set; }
public bool CanMarkComplete { get; set; }
public bool CanMarkSettled { get; set; }
public bool CanMarkInvalid { get; set; }
public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid;
public bool CanMarkStatus => CanMarkSettled || CanMarkInvalid;
}
}

View file

@ -20,9 +20,9 @@ namespace BTCPayServer.Models.InvoicingModels
public string InvoiceId { get; set; }
public InvoiceState Status { get; set; }
public bool CanMarkComplete { get; set; }
public bool CanMarkSettled { get; set; }
public bool CanMarkInvalid { get; set; }
public bool CanMarkStatus => CanMarkComplete || CanMarkInvalid;
public bool CanMarkStatus => CanMarkSettled || CanMarkInvalid;
public bool ShowCheckout { get; set; }
public string ExceptionStatus { get; set; }
public string AmountCurrency { get; set; }

View file

@ -27,5 +27,6 @@ namespace BTCPayServer.Services
public int? MigratedInvoiceTextSearchPages { get; set; }
public bool MigrateAppCustomOption { get; set; }
public bool MigratePayoutDestinationId { get; set; }
public bool AddInitialUserBlob { get; set; }
}
}

View file

@ -200,7 +200,7 @@
<p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json; Authorization: Basic YourLegacyAPIkey"</code>, Legacy API key can be created with Access Tokens in Store settings</li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is either <code>confirmed</code> or <code>complete</code></li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code></li>
<li>You can then ship your order</li>
</ul>
</p>

View file

@ -1,3 +1,4 @@
@using BTCPayServer.Client.Models
@model InvoiceDetailsModel
@{
ViewData["Title"] = $"Invoice {Model.Id}";
@ -20,7 +21,7 @@
$.post(invoiceId + "/changestate/" + newState)
.done(function (data) {
var alertClassModifier = {
"complete (marked)": 'success',
"settled (marked)": 'success',
"invalid (marked)": 'danger'
}[data.statusString];
var statusHtml = "<span class='fs-6 fw-normal badge bg-" + alertClassModifier + "'>" + data.statusString + " <span class='fa fa-check'></span></span>"
@ -63,7 +64,7 @@
}
else
{
<button href="#" class="btn btn-secondary text-nowrap" data-bs-toggle="tooltip" title="You can only issue refunds on invoices with confirmed payments" disabled><span class="fa fa-undo me-1"></span> Issue Refund</button>
<button href="#" class="btn btn-secondary text-nowrap" data-bs-toggle="tooltip" title="You can only refund an invoice that has been settled. Please wait for the transaction to confirm on the blockchain before attempting to refund it." disabled><span class="fa fa-undo me-1"></span> Issue refund</button>
}
<form class="p-0 ms-3" asp-action="ToggleArchive" asp-route-invoiceId="@Model.Id" method="post">
<button type="submit" class="btn @(Model.Archived ? "btn-warning" : "btn btn-danger")" id="btn-archive-toggle">
@ -127,6 +128,10 @@
<div class="dropdown">
<button class="btn btn-secondary btn-sm dropdown-toggle py-1 px-2" type="button" id="markStatusDropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@Model.State
@if (Model.StatusException != InvoiceExceptionStatus.None)
{
@String.Format("({0})", Model.StatusException.ToString());
}
</button>
<div class="dropdown-menu" aria-labelledby="markStatusDropdownMenuButton">
@if (Model.CanMarkInvalid)
@ -135,10 +140,10 @@
Mark as invalid <span class="fa fa-times"></span>
</a>
}
@if (Model.CanMarkComplete)
@if (Model.CanMarkSettled)
{
<a class="dropdown-item" href="#" data-id="@Model.Id" data-status="complete" data-change-invoice-status-button>
Mark as complete <span class="fa fa-check-circle"></span>
<a class="dropdown-item" href="#" data-id="@Model.Id" data-status="settled" data-change-invoice-status-button>
Mark as settled <span class="fa fa-check-circle"></span>
</a>
}
</div>
@ -147,6 +152,10 @@
else
{
@Model.State
@if (Model.StatusException != InvoiceExceptionStatus.None)
{
@String.Format(" ({0})", Model.StatusException.ToString());
}
}
</td>
</tr>
@ -384,4 +393,4 @@
</div>
</div>
</div>
</section>
</section>

View file

@ -0,0 +1,57 @@
@inject UserManager<ApplicationUser> _userManager
@* This is a temporary infobox to inform users about the state changes in 1.4.0. It should be removed eventually. *@
@if ((await _userManager.GetUserAsync(User)).GetBlob().ShowInvoiceStatusChangeHint)
{
<div class="row">
<div class="alert alert-light alert-dismissible fade show" role="alert">
<form method="post" asp-controller="Manage" asp-action="DisableShowInvoiceStatusChangeHint" id="invoicestatuschangeform">
<h6><strong>Update v1.4.0:</strong> Invoice states have been updated to match the Greenfield API.</h6>
<div class="float-start">
<ul class="list-unstyled">
<li>
<span class="badge badge-processing">Paid</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
</svg>
<span class="badge badge-processing">Processing</span>
</li>
<li>
<span class="badge badge-settled">Completed</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
</svg>
<span class="badge badge-settled">Settled</span>
</li>
<li>
<span class="badge badge-settled">Confirmed</span>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
</svg>
<span class="badge badge-settled">Settled</span>
</li>
</ul>
</div>
<div class="float-end">
<button type="button" class="btn btn-primary" data-bs-dismiss="alert">
Dismiss
</button>
<button name="command" type="submit" value="save" class="btn btn-danger" data-bs-dismiss="alert">
Don't Show Again
</button>
</div>
</form>
</div>
</div>
}
<script>
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("invoicestatuschangeform").addEventListener("submit", function (event) {
event.preventDefault();
var xhttp = new XMLHttpRequest();
xhttp.open('POST', event.target.getAttribute('action'), true);
xhttp.send(new FormData(event.target));
});
});
</script>

View file

@ -1,3 +1,4 @@
@using BTCPayServer.Client.Models
@model InvoicesModel
@{
ViewData.SetActivePageAndTitle(InvoiceNavPages.Index, "Invoices");
@ -58,14 +59,13 @@
background: #c94a47;
color: #fff;
}
.badge-confirmed,
.badge-paid {
.badge-processing {
background: #f1c332;
color: #000;
}
.badge-complete {
.badge-settled {
background: #329f80;
color: #fff;
}
@ -182,22 +182,23 @@
<div class="container">
<partial name="_StatusMessage" />
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h2 class="mb-0">
@ViewData["Title"]
<small>
<a href="https://docs.btcpayserver.org/PaymentRequests/" class="ms-1" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</small>
</h2>
<a id="CreateNewInvoice" asp-action="CreateInvoice" asp-route-searchTerm="@Model.SearchTerm" class="btn btn-primary mt-3 mt-sm-0">
<span class="fa fa-plus"></span>
Create an invoice
</a>
</div>
<div class="row">
<div class="d-sm-flex align-items-center justify-content-between mb-4">
<h2 class="mb-0">
@ViewData["Title"]
<small>
<a href="https://docs.btcpayserver.org/PaymentRequests/" class="ms-1" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</small>
</h2>
<a id="CreateNewInvoice" asp-action="CreateInvoice" asp-route-searchTerm="@Model.SearchTerm" class="btn btn-primary mt-3 mt-sm-0">
<span class="fa fa-plus"></span>
Create an invoice
</a>
</div>
<partial name="InvoiceStatusChangePartial"/>
<div class="row">
<div class="col-12 col-lg-6 mb-5 mb-lg-2 ms-auto">
<form asp-action="ListInvoices" method="get">
<input type="hidden" asp-for="Count"/>
@ -216,7 +217,7 @@
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="SearchOptionsToggle">
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="status:invalid@{@storeIds}">Invalid Invoices</a>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="status:paid,status:confirmed,status:complete@{@storeIds}">Paid Invoices</a>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="status:processing,status:settled@{@storeIds}">Paid Invoices</a>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidLate@{@storeIds}">Paid Late Invoices</a>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidPartial@{@storeIds}">Paid Partial Invoices</a>
<a class="dropdown-item" asp-action="ListInvoices" asp-route-count="@Model.Count" asp-route-searchTerm="exceptionstatus:paidOver@{@storeIds}">Paid Over Invoices</a>
@ -295,15 +296,12 @@
<li><code>storeid:id</code> for filtering a specific store</li>
<li><code>orderid:id</code> for filtering a specific order</li>
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
<li><code>status:(expired|invalid|complete|confirmed|paid|new)</code> for filtering a specific status</li>
<li><code>status:(expired|invalid|settled|processing|new)</code> for filtering a specific status</li>
<li><code>exceptionstatus:(paidover|paidlate|paidpartial)</code> for filtering a specific exception state</li>
<li><code>unusual:(true|false)</code> for filtering invoices which might requires merchant attention (those invalid or with an exceptionstatus)</li>
<li><code>startdate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created after certain date</li>
<li><code>enddate:yyyy-MM-dd HH:mm:ss</code> getting invoices that were created before certain date</li>
</ul>
<p>
If you want all confirmed and complete invoices, you can duplicate a filter <code>status:confirmed, status:complete</code>.
</p>
</div>
</div>
@ -381,9 +379,13 @@
}
@if (invoice.CanMarkStatus)
{
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.ToString().ToLower()">
<div id="pavpill_@invoice.InvoiceId" class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
<span class="dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@invoice.Status.ToString()
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString());
}
</span>
<div class="dropdown-menu pull-right">
@if (invoice.CanMarkInvalid)
@ -392,10 +394,10 @@
Mark as invalid <span class="fa fa-times"></span>
</button>
}
@if (invoice.CanMarkComplete)
@if (invoice.CanMarkSettled)
{
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="complete">
Mark as complete <span class="fa fa-check-circle"></span>
<button class="dropdown-item small cursorPointer changeInvoiceState" data-invoice-id="@invoice.InvoiceId" data-new-state="settled">
Mark as settled <span class="fa fa-check-circle"></span>
</button>
}
</div>
@ -403,8 +405,12 @@
}
else
{
<span class="badge badge-@invoice.Status.ToString().ToLower()">
@invoice.Status.ToString().ToLower()
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
@invoice.Status.Status.ToModernStatus().ToString() @* @invoice.Status.ToString().ToLower() *@
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
{
@String.Format("({0})", @invoice.Status.ExceptionStatus.ToString());
}
</span>
}
@foreach (var paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))