Skip to content

Commit 76f5ca9

Browse files
committed
Release MakePay 1.7.5 payment received UX
1 parent 6805d9a commit 76f5ca9

9 files changed

Lines changed: 182 additions & 16 deletions

File tree

BTCPayServer.Plugins.MakePay.Tests/MakePayCheckoutPolicyTests.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,4 +370,20 @@ public void CashAppCheckoutStaysInsideBtcpayAndUsesOriginScopedApi()
370370
Assert.DoesNotContain("window.location.assign", source);
371371
Assert.DoesNotContain("paymentMethod=cash_app_onramp", source);
372372
}
373+
374+
[Fact]
375+
public void DepositReceivedUsesPayerFacingConfirmationInsteadOfSendInstructions()
376+
{
377+
var source = File.ReadAllText(
378+
Path.Combine(AppContext.BaseDirectory, "Fixtures", "CheckoutPaymentMethodExtension.cshtml"));
379+
380+
Assert.Contains("v-else-if=\"paymentStarted && isPaymentReceived\"", source);
381+
Assert.Contains("Payment received", source);
382+
Assert.Contains("We received your payment and are finalizing the invoice", source);
383+
Assert.Contains("['deposit_received', 'swapping', 'sending', 'complete']", source);
384+
Assert.Contains("paymentReceived: false", source);
385+
Assert.Contains("this.paymentReceived = true", source);
386+
Assert.Contains("Do not send another payment to this address", source);
387+
Assert.DoesNotContain("Status: {{ status }}", source);
388+
}
373389
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#nullable enable
2+
using BTCPayServer.Data;
3+
using BTCPayServer.Plugins.MakePay.Services;
4+
using Xunit;
5+
6+
namespace BTCPayServer.Plugins.MakePay.Tests;
7+
8+
public class MakePayPaymentRecorderTests
9+
{
10+
[Theory]
11+
[InlineData("deposit_received")]
12+
[InlineData("swapping")]
13+
[InlineData("sending")]
14+
[InlineData("complete")]
15+
[InlineData(" DEPOSIT_RECEIVED ")]
16+
public void DepositReceiptAndLaterStatesCreateAProcessingPayment(string status)
17+
{
18+
Assert.Equal(
19+
PaymentStatus.Processing,
20+
MakePayPaymentRecorder.MapSessionStatusToPaymentStatus(status));
21+
}
22+
23+
[Theory]
24+
[InlineData(null)]
25+
[InlineData("")]
26+
[InlineData("quoted")]
27+
[InlineData("awaiting_deposit")]
28+
[InlineData("pending")]
29+
[InlineData("underpaid")]
30+
[InlineData("expired")]
31+
[InlineData("failed")]
32+
[InlineData("cancelled")]
33+
[InlineData("refunded")]
34+
public void PreDepositAndTerminalFailureStatesDoNotCreateAPayment(string? status)
35+
{
36+
Assert.Null(MakePayPaymentRecorder.MapSessionStatusToPaymentStatus(status));
37+
}
38+
}

BTCPayServer.Plugins.MakePay/BTCPayServer.Plugins.MakePay.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<PropertyGroup>
1010
<Product>Accept more than 90+ coins and chains - MakePay</Product>
1111
<Description>Accept 90+ altcoins with Makepay’s fully decentralized BTCPay plugin. Customers pay in their preferred coin; merchants receive their chosen asset via instant conversion.</Description>
12-
<Version>1.7.4</Version>
12+
<Version>1.7.5</Version>
1313
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
1414
</PropertyGroup>
1515

BTCPayServer.Plugins.MakePay/Controllers/MakePayWebhookController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public async Task<IActionResult> HandleWebhook(string storeId)
150150
return Ok();
151151
}
152152

153-
await _paymentRecorder.RecordIfComplete(
153+
await _paymentRecorder.RecordIfReceived(
154154
invoice,
155155
promptDetails,
156156
session,

BTCPayServer.Plugins.MakePay/MakePayPlugin.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace BTCPayServer.Plugins.MakePay;
1010

1111
public class MakePayPlugin : BaseBTCPayServerPlugin
1212
{
13-
public const string PluginVersion = "1.7.4";
13+
public const string PluginVersion = "1.7.5";
1414
public static readonly PaymentMethodId MakePayPaymentMethodId = new("MAKEPAY");
1515

1616
public override IBTCPayServerPlugin.PluginDependency[] Dependencies { get; } =

BTCPayServer.Plugins.MakePay/Services/MakePayInvoiceListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,6 @@ private async Task ReconcileInvoice(
191191
["settlementAmount"] = current["settlementAmount"]
192192
};
193193

194-
await _paymentRecorder.RecordIfComplete(invoice, promptDetails, session);
194+
await _paymentRecorder.RecordIfReceived(invoice, promptDetails, session);
195195
}
196196
}

BTCPayServer.Plugins.MakePay/Services/MakePayPaymentRecorder.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,26 @@ public MakePayPaymentRecorder(
3232
_logger = logger;
3333
}
3434

35-
public async Task<bool> RecordIfComplete(
35+
public async Task<bool> RecordIfReceived(
3636
InvoiceEntity invoice,
3737
MakePayPromptDetails promptDetails,
3838
JObject session,
3939
string? deliveryId = null)
4040
{
4141
var status = Text(session["status"]);
42-
if (!string.Equals(status, "complete", StringComparison.OrdinalIgnoreCase))
42+
var paymentStatus = MapSessionStatusToPaymentStatus(status);
43+
if (paymentStatus is null)
4344
{
4445
return false;
4546
}
4647

4748
var sessionId = Text(session["id"]) ?? Text(session["sessionId"]);
4849
if (string.IsNullOrWhiteSpace(sessionId))
4950
{
50-
_logger.LogWarning("MakePay complete session for invoice {InvoiceId} did not include a session id.", invoice.Id);
51+
_logger.LogWarning(
52+
"MakePay {Status} session for invoice {InvoiceId} did not include a session id.",
53+
status,
54+
invoice.Id);
5155
return false;
5256
}
5357

@@ -70,7 +74,7 @@ public async Task<bool> RecordIfComplete(
7074
{
7175
PaymentLinkUid = promptDetails.PaymentLinkUid,
7276
SessionId = sessionId,
73-
Status = status ?? "complete",
77+
Status = status!,
7478
PaymentMethod = Text(session["paymentMethod"]) ??
7579
Text(session.SelectToken("quotePayload.paymentMethod")) ??
7680
"crypto",
@@ -107,7 +111,9 @@ public async Task<bool> RecordIfComplete(
107111
InvoiceDataId = invoice.Id,
108112
Currency = "BTC",
109113
Amount = promptDetails.BtcAmount,
110-
Status = PaymentStatus.Processing,
114+
// A source-chain deposit is real received-payment evidence, but it is
115+
// not proof that the merchant-controlled settlement has confirmed.
116+
Status = paymentStatus.Value,
111117
Created = DateTimeOffset.UtcNow
112118
};
113119
payment.Set(invoice, handler, paymentData);
@@ -157,6 +163,19 @@ public async Task<bool> RecordIfComplete(
157163
return true;
158164
}
159165

166+
internal static PaymentStatus? MapSessionStatusToPaymentStatus(string? status)
167+
{
168+
// A webhook can be missed while the session advances, so every state at
169+
// or beyond deposit receipt must be able to create the same idempotent
170+
// native payment record. It intentionally remains Processing; BTCPay may
171+
// settle the invoice only from independent settlement confirmation.
172+
return status?.Trim().ToLowerInvariant() switch
173+
{
174+
"deposit_received" or "swapping" or "sending" or "complete" => PaymentStatus.Processing,
175+
_ => null
176+
};
177+
}
178+
160179
private static string? Text(JToken? token)
161180
{
162181
var value = token?.Value<string>()?.Trim();

BTCPayServer.Plugins.MakePay/Views/Shared/MakePay/CheckoutPaymentMethodExtension.cshtml

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,30 @@
142142
font-size: .75rem;
143143
}
144144
145+
.makepay-checkout .makepay-payment-received {
146+
width: 100%;
147+
max-width: 28rem;
148+
margin: 0 auto;
149+
padding: 2rem 1rem;
150+
}
151+
152+
.makepay-checkout .makepay-payment-received-icon {
153+
display: inline-flex;
154+
align-items: center;
155+
justify-content: center;
156+
width: 4rem;
157+
height: 4rem;
158+
margin-bottom: 1rem;
159+
border-radius: 50%;
160+
background: var(--btcpay-success-dim-bg, rgba(60, 153, 110, .18));
161+
color: var(--btcpay-success, #3c996e);
162+
}
163+
164+
.makepay-checkout .makepay-payment-received-icon svg {
165+
width: 2rem;
166+
height: 2rem;
167+
}
168+
145169
@@media (max-width: 420px) {
146170
.makepay-checkout .makepay-option > span {
147171
grid-template-columns: 2.5rem minmax(0, 1fr);
@@ -382,6 +406,23 @@
382406
</div>
383407
</div>
384408

409+
<div v-else-if="paymentStarted && isPaymentReceived"
410+
class="makepay-payment-received text-center"
411+
role="status"
412+
aria-live="polite">
413+
<span class="makepay-payment-received-icon" aria-hidden="true">
414+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
415+
<path d="M5 12.5l4.25 4.25L19 7"
416+
stroke="currentColor"
417+
stroke-width="2.5"
418+
stroke-linecap="round"
419+
stroke-linejoin="round"></path>
420+
</svg>
421+
</span>
422+
<div class="h4 mb-2">{{ paymentReceivedTitle }}</div>
423+
<p class="text-muted mb-0">{{ paymentReceivedMessage }}</p>
424+
</div>
425+
385426
<div v-else-if="paymentStarted && isCashAppPayment"
386427
class="makepay-cash-app-transfer text-center">
387428
<div class="makepay-cash-app-summary justify-content-center mb-3">
@@ -452,8 +493,11 @@
452493
</button>
453494
</div>
454495

455-
<div v-if="status && !isCashAppPayment" class="mt-3 small text-muted text-center">
456-
Status: {{ status }}
496+
<div v-if="status && !isCashAppPayment && !isPaymentReceived"
497+
class="mt-3 small text-muted text-center"
498+
role="status"
499+
aria-live="polite">
500+
{{ checkoutStatusLabel }}
457501
</div>
458502

459503
<div v-if="isBusy" class="text-center py-3">
@@ -499,6 +543,7 @@
499543
cashAppPaymentRequest: '',
500544
paymentExpiresAtMs: 0,
501545
status: '',
546+
paymentReceived: false,
502547
error: '',
503548
success: '',
504549
isBusy: false,
@@ -615,6 +660,44 @@
615660
};
616661
return labels[this.status] || 'Waiting for payment';
617662
},
663+
isPaymentReceived: function () {
664+
return this.paymentReceived ||
665+
['deposit_received', 'swapping', 'sending', 'complete']
666+
.indexOf(this.status) >= 0;
667+
},
668+
paymentReceivedTitle: function () {
669+
if (['expired', 'failed', 'cancelled', 'refunded'].indexOf(this.status) >= 0) {
670+
return 'Payment needs attention';
671+
}
672+
673+
return this.status === 'complete'
674+
? 'Payment complete'
675+
: 'Payment received';
676+
},
677+
paymentReceivedMessage: function () {
678+
if (['expired', 'failed', 'cancelled', 'refunded'].indexOf(this.status) >= 0) {
679+
return 'We received your transfer, but could not finish processing it. Do not send another payment to this address. Please contact the merchant for help.';
680+
}
681+
682+
return this.status === 'complete'
683+
? 'Thank you. This invoice is paid and you can safely close this window.'
684+
: 'Thank you. We received your payment and are finalizing the invoice. This page will update automatically.';
685+
},
686+
checkoutStatusLabel: function () {
687+
var labels = {
688+
quoted: 'Quote ready',
689+
awaiting_deposit: 'Waiting for payment',
690+
pending: 'Waiting for payment',
691+
deposit_received: 'Payment received',
692+
swapping: 'Processing payment',
693+
sending: 'Processing payment',
694+
complete: 'Payment complete',
695+
expired: 'Payment expired',
696+
failed: 'Payment failed',
697+
cancelled: 'Payment cancelled'
698+
};
699+
return labels[this.status] || 'Checking payment status';
700+
},
618701
refundAddressMode: function () {
619702
return this.model.makePayRefundAddressMode === 'payer_entered'
620703
? 'payer_entered'
@@ -1116,6 +1199,7 @@
11161199
this.cashAppPaymentRequest = '';
11171200
this.paymentExpiresAtMs = 0;
11181201
this.status = '';
1202+
this.paymentReceived = false;
11191203
this.error = '';
11201204
this.success = '';
11211205
this.awaitingConfirmation = false;
@@ -1363,7 +1447,13 @@
13631447
this.selectedAsset = String(payload.selectedSellAsset);
13641448
this.syncSelectedCurrencyFromAsset();
13651449
}
1366-
this.status = payload.status || this.status;
1450+
var nextStatus = payload.status || this.status;
1451+
if (['deposit_received', 'swapping', 'sending', 'complete'].indexOf(nextStatus) >= 0) {
1452+
// Once the payer transfer is observed, never expose payment
1453+
// instructions again because of a weaker tracking response.
1454+
this.paymentReceived = true;
1455+
}
1456+
this.status = nextStatus;
13671457
this.applyQuotedAmounts(payload);
13681458
this.paymentExpiresAtMs = this.parseDateMs(payload.expiresAt) || this.paymentExpiresAtMs;
13691459
if (payload.deposit) {

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,16 +182,19 @@ polling, and quote refresh.
182182
## Settlement and Accounting
183183

184184
MakePay session webhooks and reconciliation responses are treated as evidence
185-
that the MakePay payment session completed. The plugin records that evidence as
186-
a **Processing** MakePay payment on the invoice, so BTCPay does not mark the
187-
invoice as settled solely because MakePay reported a completed session.
185+
that the payer's source-chain deposit was received. As soon as the session
186+
reaches `deposit_received` (or a later payment state), the plugin records a
187+
**Processing** MakePay payment and publishes BTCPay's native **Invoice - Received
188+
Payment** event. The payment deliberately remains Processing, so BTCPay does not
189+
mark the invoice as settled without independent settlement confirmation.
188190

189191
BTCPay final settlement should be based on independent BTC wallet/on-chain
190192
confirmation for the merchant-controlled settlement address.
191193

192194
| MakePay state | BTCPay behavior |
193195
| ----------------------------------- | ------------------------------------------------------------------------------ |
194-
| `complete` | Records a Processing MakePay payment with session and deposit metadata. |
196+
| `deposit_received` | Records a Processing payment and publishes the native Received Payment event. |
197+
| `swapping`, `sending`, or `complete` | Records the same payment if an earlier status callback was missed. |
195198
| `failed`, `expired`, or `cancelled` | Shows the state without recording a settled payment. |
196199
| `underpaid` | Keeps the prompt unsettled so the invoice can be handled by policy or support. |
197200

0 commit comments

Comments
 (0)