mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-19 05:33:31 +01:00
Pull Payment: Add QR scanner for destination and infer payment method (#5358)
* Pull Payment: Add QR scanner for destination and infer payment method Closes #4754. * Test fix
This commit is contained in:
parent
229a4ea56c
commit
e5a2aeb145
@ -1979,17 +1979,15 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
|
||||
// Bitcoin-only, SelectedPaymentMethod should not be displayed
|
||||
s.Driver.ElementDoesNotExist(By.Id("SelectedPaymentMethod"));
|
||||
|
||||
var bolt = (await s.Server.CustomerLightningD.CreateInvoice(
|
||||
payoutAmount,
|
||||
$"LN payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None)).BOLT11;
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
|
||||
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(
|
||||
$"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike)}]"))
|
||||
.Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
|
||||
//we do not allow short-life bolts.
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error);
|
||||
@ -2000,11 +1998,6 @@ namespace BTCPayServer.Tests
|
||||
TimeSpan.FromDays(31), CancellationToken.None)).BOLT11;
|
||||
s.Driver.FindElement(By.Id("Destination")).Clear();
|
||||
s.Driver.FindElement(By.Id("Destination")).SendKeys(bolt);
|
||||
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(
|
||||
$"#SelectedPaymentMethod option[value={new PaymentMethodId("BTC", PaymentTypes.LightningLike)}]"))
|
||||
.Click();
|
||||
|
||||
s.Driver.FindElement(By.Id("ClaimedAmount")).SendKeys(Keys.Enter);
|
||||
s.FindAlertMessage();
|
||||
|
||||
|
@ -69,14 +69,14 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var payouts = (await ctx.Payouts.GetPayoutInPeriod(pp)
|
||||
.OrderByDescending(o => o.Date)
|
||||
.ToListAsync())
|
||||
.Select(o => new
|
||||
{
|
||||
Entity = o,
|
||||
Blob = o.GetBlob(_serializerSettings),
|
||||
ProofBlob = _payoutHandlers.FindPayoutHandler(o.GetPaymentMethodId())?.ParseProof(o)
|
||||
});
|
||||
.OrderByDescending(o => o.Date)
|
||||
.ToListAsync())
|
||||
.Select(o => new
|
||||
{
|
||||
Entity = o,
|
||||
Blob = o.GetBlob(_serializerSettings),
|
||||
ProofBlob = _payoutHandlers.FindPayoutHandler(o.GetPaymentMethodId())?.ParseProof(o)
|
||||
});
|
||||
var cd = _currencyNameTable.GetCurrencyData(blob.Currency, false);
|
||||
var totalPaid = payouts.Where(p => p.Entity.State != PayoutState.Cancelled).Select(p => p.Blob.Amount).Sum();
|
||||
var amountDue = blob.Limit - totalPaid;
|
||||
@ -91,18 +91,17 @@ namespace BTCPayServer.Controllers
|
||||
CurrencyData = cd,
|
||||
StartDate = pp.StartDate,
|
||||
LastRefreshed = DateTime.UtcNow,
|
||||
Payouts = payouts
|
||||
.Select(entity => new ViewPullPaymentModel.PayoutLine
|
||||
{
|
||||
Id = entity.Entity.Id,
|
||||
Amount = entity.Blob.Amount,
|
||||
Currency = blob.Currency,
|
||||
Status = entity.Entity.State,
|
||||
Destination = entity.Blob.Destination,
|
||||
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId),
|
||||
Link = entity.ProofBlob?.Link,
|
||||
TransactionId = entity.ProofBlob?.Id
|
||||
}).ToList()
|
||||
Payouts = payouts.Select(entity => new ViewPullPaymentModel.PayoutLine
|
||||
{
|
||||
Id = entity.Entity.Id,
|
||||
Amount = entity.Blob.Amount,
|
||||
Currency = blob.Currency,
|
||||
Status = entity.Entity.State,
|
||||
Destination = entity.Blob.Destination,
|
||||
PaymentMethod = PaymentMethodId.Parse(entity.Entity.PaymentMethodId),
|
||||
Link = entity.ProofBlob?.Link,
|
||||
TransactionId = entity.ProofBlob?.Id
|
||||
}).ToList()
|
||||
};
|
||||
vm.IsPending &= vm.AmountDue > 0.0m;
|
||||
|
||||
@ -176,31 +175,53 @@ namespace BTCPayServer.Controllers
|
||||
[HttpPost("pull-payments/{pullPaymentId}/claim")]
|
||||
public async Task<IActionResult> ClaimPullPayment(string pullPaymentId, ViewPullPaymentModel vm, CancellationToken cancellationToken)
|
||||
{
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var pp = await ctx.PullPayments.FindAsync(pullPaymentId);
|
||||
if (pp is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(pullPaymentId), "This pull payment does not exists");
|
||||
}
|
||||
|
||||
var ppBlob = pp.GetBlob();
|
||||
|
||||
var paymentMethodId = ppBlob.SupportedPaymentMethods.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
|
||||
|
||||
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
|
||||
if (payoutHandler is null)
|
||||
if (string.IsNullOrEmpty(vm.Destination))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.SelectedPaymentMethod), "Invalid destination with selected payment method");
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Please provide a destination");
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
var destination = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken);
|
||||
if (destination.destination is null)
|
||||
|
||||
var ppBlob = pp.GetBlob();
|
||||
var supported = ppBlob.SupportedPaymentMethods;
|
||||
PaymentMethodId paymentMethodId = null;
|
||||
IClaimDestination destination = null;
|
||||
if (string.IsNullOrEmpty(vm.SelectedPaymentMethod))
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), destination.error ?? "Invalid destination with selected payment method");
|
||||
foreach (var pmId in supported)
|
||||
{
|
||||
var handler = _payoutHandlers.FindPayoutHandler(pmId);
|
||||
(IClaimDestination dst, string err) = handler == null
|
||||
? (null, "No payment handler found for this payment method")
|
||||
: await handler.ParseAndValidateClaimDestination(pmId, vm.Destination, ppBlob, cancellationToken);
|
||||
if (dst is not null && err is null)
|
||||
{
|
||||
paymentMethodId = pmId;
|
||||
destination = dst;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
paymentMethodId = supported.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
|
||||
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
|
||||
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken)).destination;
|
||||
}
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
var amtError = ClaimRequest.IsPayoutAmountOk(destination.destination, vm.ClaimedAmount == 0? null: vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
|
||||
var amtError = ClaimRequest.IsPayoutAmountOk(destination, vm.ClaimedAmount == 0? null: vm.ClaimedAmount, paymentMethodId.CryptoCode, ppBlob.Currency);
|
||||
if (amtError.error is not null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ClaimedAmount), amtError.error );
|
||||
@ -215,9 +236,9 @@ namespace BTCPayServer.Controllers
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
var result = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
||||
var result = await _pullPaymentHostedService.Claim(new ClaimRequest
|
||||
{
|
||||
Destination = destination.destination,
|
||||
Destination = destination,
|
||||
PullPaymentId = pullPaymentId,
|
||||
Value = vm.ClaimedAmount,
|
||||
PaymentMethodId = paymentMethodId
|
||||
@ -225,14 +246,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
if (result.Result != ClaimRequest.ClaimResult.Ok)
|
||||
{
|
||||
if (result.Result == ClaimRequest.ClaimResult.AmountTooLow)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.ClaimedAmount), ClaimRequest.GetErrorMessage(result.Result));
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, ClaimRequest.GetErrorMessage(result.Result));
|
||||
}
|
||||
ModelState.AddModelError(
|
||||
result.Result == ClaimRequest.ClaimResult.AmountTooLow ? nameof(vm.ClaimedAmount) : string.Empty,
|
||||
ClaimRequest.GetErrorMessage(result.Result));
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ namespace BTCPayServer.Models
|
||||
StoreId = data.StoreId;
|
||||
var blob = data.GetBlob();
|
||||
PaymentMethods = blob.SupportedPaymentMethods;
|
||||
BitcoinOnly = blob.SupportedPaymentMethods.All(p => p.CryptoCode == "BTC");
|
||||
SelectedPaymentMethod = PaymentMethods.First().ToString();
|
||||
Archived = data.Archived;
|
||||
AutoApprove = blob.AutoApproveClaims;
|
||||
@ -66,6 +67,8 @@ namespace BTCPayServer.Models
|
||||
}
|
||||
}
|
||||
|
||||
public bool BitcoinOnly { get; set; }
|
||||
|
||||
public string StoreId { get; set; }
|
||||
|
||||
public string SelectedPaymentMethod { get; set; }
|
||||
|
@ -30,6 +30,7 @@
|
||||
<partial name="LayoutHead" />
|
||||
<partial name="LayoutHeadStoreBranding" model="@(Model.BrandColor, Model.CssFileId, Model.CustomCSSLink, Model.EmbeddedCSS)" />
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.min.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||
<style>
|
||||
.no-marker > ul { list-style-type: none; }
|
||||
</style>
|
||||
@ -46,7 +47,7 @@
|
||||
<div class="input-group">
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
<button type="button" class="btn btn-secondary" id="lnurlwithdraw-button">
|
||||
<button type="button" class="btn btn-secondary only-for-js" id="lnurlwithdraw-button">
|
||||
<span class="fa fa-qrcode fa-2x" title="LNURL-Withdraw"></span>
|
||||
</button>
|
||||
}
|
||||
@ -56,13 +57,15 @@
|
||||
<input type="hidden" asp-for="SelectedPaymentMethod">
|
||||
<span class="input-group-text">@Model.PaymentMethods.First().ToPrettyString()</span>
|
||||
}
|
||||
else
|
||||
else if (!Model.BitcoinOnly)
|
||||
{
|
||||
<select class="form-select w-auto" asp-for="SelectedPaymentMethod" asp-items="Model.PaymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString()))"></select>
|
||||
}
|
||||
<button type="button" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan destination with camera" id="scandestination-button">
|
||||
<i class="fa fa-camera"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-3 col-sm-6 mb-sm-0 col-lg-3">
|
||||
<div class="input-group">
|
||||
<input type="number" inputmode="decimal" class="form-control form-control-lg text-end hide-number-spin" asp-for="ClaimedAmount" max="@Model.AmountDue" min="@Model.MinimumClaim" step="any" placeholder="Amount" required>
|
||||
@ -92,22 +95,21 @@
|
||||
{
|
||||
<h2 class="h4 mb-3">@Model.Title</h2>
|
||||
}
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-muted text-nowrap">Start Date</span>
|
||||
|
||||
<span class="text-nowrap">@Model.StartDate.ToString("g")</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="text-muted text-nowrap">Last Updated</span>
|
||||
|
||||
<span class="text-nowrap">@Model.LastRefreshed.ToString("g")</span>
|
||||
<button type="button" class="btn btn-link fw-semibold d-none d-lg-inline-block d-print-none border-0 p-0 ms-4 only-for-js" id="copyLink">
|
||||
</div>
|
||||
<div class="d-flex align-items-center only-for-js gap-3 my-3">
|
||||
<button type="button" class="btn btn-link fw-semibold d-print-none p-0" id="copyLink">
|
||||
Copy Link
|
||||
</button>
|
||||
<button type="button" class="btn btn-link fw-semibold d-inline-block d-print-none border-0 p-0 ms-4 only-for-js" page-qr>
|
||||
<span class="fa fa-qrcode"></span> Show QR
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-link fw-semibold d-print-none p-0" page-qr>
|
||||
<span class="fa fa-qrcode"></span> Show QR
|
||||
</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.ResetIn))
|
||||
{
|
||||
@ -207,10 +209,13 @@
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
<partial name="LayoutFoot" />
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<partial name="ShowQR" />
|
||||
<partial name="CameraScanner"/>
|
||||
<partial name="LayoutFoot" />
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode-reader/VueQrcodeReader.umd.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/ur-registry/urlib.min.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
window.qrApp = initQRShow({});
|
||||
@ -219,6 +224,12 @@
|
||||
qrApp.note = "Scan this QR code to open this page on your mobile device.";
|
||||
qrApp.showData(window.location.href);
|
||||
});
|
||||
|
||||
delegate('click', '#copyLink', window.copyUrlToClipboard);
|
||||
|
||||
initCameraScanningApp("Scan address/ payment link", data => {
|
||||
document.getElementById("Destination").value = data;
|
||||
}, "scanModal");
|
||||
});
|
||||
</script>
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
@ -246,11 +257,6 @@
|
||||
});
|
||||
</script>
|
||||
}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById("copyLink").addEventListener("click", window.copyUrlToClipboard);
|
||||
});
|
||||
</script>
|
||||
<vc:ui-extension-point location="pullpayment-foot" model="@Model"></vc:ui-extension-point>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user