mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
Improve Refund Flow (#3731)
This commit is contained in:
parent
fcd6159b42
commit
5616b7550f
7 changed files with 224 additions and 180 deletions
|
@ -11,18 +11,10 @@ using BTCPayServer.Models.AppViewModels;
|
|||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using BTCPayServer.Views.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
using NBitcoin.Scripting.Parser;
|
||||
using NBitpayClient;
|
||||
using NBXplorer.DerivationStrategy;
|
||||
using NBXplorer.Models;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
|
@ -358,7 +350,7 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
if (multiCurrency)
|
||||
user.RegisterDerivationScheme("LTC");
|
||||
foreach (var rateSelection in new[] { "FiatTextRadio", "CurrentRateTextRadio", "RateThenTextRadio" })
|
||||
foreach (var rateSelection in new[] { "FiatOption", "CurrentRateOption", "RateThenOption", "CustomOption" })
|
||||
await CanCreateRefundsCore(s, user, multiCurrency, rateSelection);
|
||||
}
|
||||
}
|
||||
|
@ -368,7 +360,7 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
s.GoToHome();
|
||||
s.Server.PayTester.ChangeRate("BTC_USD", new Rating.BidAsk(5000.0m, 5100.0m));
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new NBitpayClient.Invoice()
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice
|
||||
{
|
||||
Currency = "USD",
|
||||
Price = 5000.0m
|
||||
|
@ -390,26 +382,35 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("BOLT11Expiration")).Clear();
|
||||
s.Driver.FindElement(By.Id("BOLT11Expiration")).SendKeys("5" + Keys.Enter);
|
||||
s.GoToInvoice(invoice.Id);
|
||||
s.Driver.FindElement(By.Id("refundlink")).Click();
|
||||
s.Driver.FindElement(By.Id("IssueRefund")).Click();
|
||||
|
||||
if (multiCurrency)
|
||||
{
|
||||
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
|
||||
s.Driver.WaitUntilAvailable(By.Id("SelectedPaymentMethod"), TimeSpan.FromSeconds(1));
|
||||
s.Driver.FindElement(By.Id("SelectedPaymentMethod")).SendKeys("BTC" + Keys.Enter);
|
||||
s.Driver.FindElement(By.Id("ok")).Click();
|
||||
}
|
||||
|
||||
s.Driver.WaitUntilAvailable(By.Id("RefundForm"), TimeSpan.FromSeconds(1));
|
||||
Assert.Contains("$5,500.00", s.Driver.PageSource); // Should propose reimburse in fiat
|
||||
Assert.Contains("1.10000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the rate of before
|
||||
Assert.Contains("2.20000000 ₿", s.Driver.PageSource); // Should propose reimburse in BTC at the current rate
|
||||
s.Driver.FindElement(By.Id(rateSelection)).Click();
|
||||
s.Driver.WaitForAndClick(By.Id(rateSelection));
|
||||
s.Driver.FindElement(By.Id("ok")).Click();
|
||||
|
||||
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
|
||||
Assert.Contains("pull-payments", s.Driver.Url);
|
||||
if (rateSelection == "FiatTextRadio")
|
||||
if (rateSelection == "FiatOption")
|
||||
Assert.Contains("$5,500.00", s.Driver.PageSource);
|
||||
if (rateSelection == "CurrentRateTextRadio")
|
||||
if (rateSelection == "CurrentOption")
|
||||
Assert.Contains("2.20000000 ₿", s.Driver.PageSource);
|
||||
if (rateSelection == "RateThenTextRadio")
|
||||
if (rateSelection == "RateThenOption")
|
||||
Assert.Contains("1.10000000 ₿", s.Driver.PageSource);
|
||||
|
||||
s.GoToInvoice(invoice.Id);
|
||||
s.Driver.FindElement(By.Id("refundlink")).Click();
|
||||
s.Driver.FindElement(By.Id("IssueRefund")).Click();
|
||||
s.Driver.WaitUntilAvailable(By.Id("Destination"), TimeSpan.FromSeconds(1));
|
||||
Assert.Contains("pull-payments", s.Driver.Url);
|
||||
var client = await user.CreateClient();
|
||||
var ppid = s.Driver.Url.Split('/').Last();
|
||||
|
|
|
@ -136,36 +136,44 @@ retry:
|
|||
ScrollTo(driver, driver.FindElement(selector));
|
||||
}
|
||||
|
||||
public static void WaitForAndClick(this IWebDriver driver, By selector)
|
||||
public static void WaitUntilAvailable(this IWebDriver driver, By selector, TimeSpan? waitTime = null)
|
||||
{
|
||||
// Try fast path
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
try
|
||||
{
|
||||
driver.FindElement(selector).Click();
|
||||
var el = driver.FindElement(selector);
|
||||
wait.Until(_ => el.Displayed && el.Enabled);
|
||||
return;
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Sometimes, selenium complain, so we enter hack territory
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
wait.UntilJsIsReady();
|
||||
|
||||
int retriesLeft = 4;
|
||||
retry:
|
||||
retry:
|
||||
try
|
||||
{
|
||||
var el = driver.FindElement(selector);
|
||||
wait.Until(d => el.Displayed && el.Enabled);
|
||||
wait.Until(_ => el.Displayed && el.Enabled);
|
||||
driver.ScrollTo(selector);
|
||||
driver.FindElement(selector).Click();
|
||||
driver.FindElement(selector);
|
||||
}
|
||||
catch (ElementClickInterceptedException) when (retriesLeft > 0)
|
||||
catch (NoSuchElementException) when (retriesLeft > 0)
|
||||
{
|
||||
retriesLeft--;
|
||||
if (waitTime != null) Thread.Sleep(waitTime.Value);
|
||||
goto retry;
|
||||
}
|
||||
wait.UntilJsIsReady();
|
||||
}
|
||||
|
||||
public static void WaitForAndClick(this IWebDriver driver, By selector)
|
||||
{
|
||||
driver.WaitUntilAvailable(selector);
|
||||
driver.FindElement(selector).Click();
|
||||
}
|
||||
|
||||
public static void SetCheckbox(this IWebDriver driver, By selector, bool value)
|
||||
{
|
||||
|
|
|
@ -198,7 +198,7 @@ namespace BTCPayServer.Controllers
|
|||
// TODO: What if no option?
|
||||
var refund = new RefundModel
|
||||
{
|
||||
Title = "Select a payment method",
|
||||
Title = "Payment method",
|
||||
AvailablePaymentMethods =
|
||||
new SelectList(options.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString())),
|
||||
"Value", "Text"),
|
||||
|
@ -210,7 +210,7 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
return await Refund(invoiceId, refund, cancellationToken);
|
||||
}
|
||||
return View(refund);
|
||||
return View("_RefundModal", refund);
|
||||
}
|
||||
|
||||
[HttpPost("invoices/{invoiceId}/refund")]
|
||||
|
@ -237,7 +237,7 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
case RefundSteps.SelectPaymentMethod:
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
model.Title = "What to refund?";
|
||||
model.Title = "How much to refund?";
|
||||
var pms = invoice.GetPaymentMethods();
|
||||
var paymentMethod = pms.SingleOrDefault(method => method.GetId() == paymentMethodId);
|
||||
|
||||
|
@ -263,7 +263,7 @@ namespace BTCPayServer.Controllers
|
|||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
return View(model);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
model.CryptoAmountNow = Math.Round(paidCurrency / rateResult.BidAsk.Bid, paymentMethodDivisibility);
|
||||
|
@ -271,9 +271,10 @@ namespace BTCPayServer.Controllers
|
|||
_CurrencyNameTable.DisplayFormatCurrency(model.CryptoAmountNow, paymentMethodId.CryptoCode);
|
||||
model.FiatAmount = paidCurrency;
|
||||
}
|
||||
|
||||
model.CustomAmount = model.FiatAmount;
|
||||
model.CustomCurrency = invoice.Currency;
|
||||
model.FiatText = _CurrencyNameTable.DisplayFormatCurrency(model.FiatAmount, invoice.Currency);
|
||||
return View(model);
|
||||
return View("_RefundModal", model);
|
||||
|
||||
case RefundSteps.SelectRate:
|
||||
createPullPayment = new CreatePullPayment
|
||||
|
@ -281,8 +282,7 @@ namespace BTCPayServer.Controllers
|
|||
Name = $"Refund {invoice.Id}",
|
||||
PaymentMethodIds = new[] { paymentMethodId },
|
||||
StoreId = invoice.StoreId,
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration,
|
||||
//AutoApproveClaims = true
|
||||
BOLT11Expiration = store.GetStoreBlob().RefundBOLT11Expiration
|
||||
};
|
||||
switch (model.SelectedRefundOption)
|
||||
{
|
||||
|
@ -291,84 +291,82 @@ namespace BTCPayServer.Controllers
|
|||
createPullPayment.Amount = model.CryptoAmountThen;
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case "CurrentRate":
|
||||
createPullPayment.Currency = paymentMethodId.CryptoCode;
|
||||
createPullPayment.Amount = model.CryptoAmountNow;
|
||||
createPullPayment.AutoApproveClaims = true;
|
||||
break;
|
||||
|
||||
case "Fiat":
|
||||
createPullPayment.Currency = invoice.Currency;
|
||||
createPullPayment.Amount = model.FiatAmount;
|
||||
createPullPayment.AutoApproveClaims = false;
|
||||
break;
|
||||
|
||||
case "Custom":
|
||||
model.Title = "How much to refund?";
|
||||
model.CustomCurrency = invoice.Currency;
|
||||
model.CustomAmount = model.FiatAmount;
|
||||
model.RefundStep = RefundSteps.SelectCustomAmount;
|
||||
return View(model);
|
||||
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
|
||||
if (model.CustomAmount <= 0)
|
||||
{
|
||||
model.AddModelError(refundModel => refundModel.CustomAmount, "Amount must be greater than 0", this);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.CustomCurrency) ||
|
||||
_CurrencyNameTable.GetCurrencyData(model.CustomCurrency, false) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CustomCurrency), "Invalid currency");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
rateResult = await _RateProvider.FetchRate(
|
||||
new CurrencyPair(paymentMethodId.CryptoCode, model.CustomCurrency), rules,
|
||||
cancellationToken);
|
||||
|
||||
//TODO: What if fetching rate failed?
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
createPullPayment.Currency = model.CustomCurrency;
|
||||
createPullPayment.Amount = model.CustomAmount;
|
||||
createPullPayment.AutoApproveClaims = paymentMethodId.CryptoCode == model.CustomCurrency;
|
||||
break;
|
||||
|
||||
default:
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Please select an option before proceeding");
|
||||
return View(model);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
break;
|
||||
case RefundSteps.SelectCustomAmount:
|
||||
if (model.CustomAmount <= 0)
|
||||
{
|
||||
model.AddModelError(refundModel => refundModel.CustomAmount, "Amount must be greater than 0", this);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.CustomCurrency) ||
|
||||
_CurrencyNameTable.GetCurrencyData(model.CustomCurrency, false) == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CustomCurrency), "Invalid currency");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
rules = store.GetStoreBlob().GetRateRules(_NetworkProvider);
|
||||
rateResult = await _RateProvider.FetchRate(
|
||||
new CurrencyPair(paymentMethodId.CryptoCode, model.CustomCurrency), rules,
|
||||
cancellationToken);
|
||||
//TODO: What if fetching rate failed?
|
||||
if (rateResult.BidAsk is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption),
|
||||
$"Impossible to fetch rate: {rateResult.EvaluatedRule}");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
createPullPayment = new CreatePullPayment
|
||||
{
|
||||
Name = $"Refund {invoice.Id}",
|
||||
PaymentMethodIds = new[] {paymentMethodId},
|
||||
StoreId = invoice.StoreId,
|
||||
Currency = model.CustomCurrency,
|
||||
Amount = model.CustomAmount,
|
||||
AutoApproveClaims = paymentMethodId.CryptoCode == model.CustomCurrency
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
var ppId = await _paymentHostedService.CreatePullPayment(createPullPayment);
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Html = "Refund successfully created!<br />Share the link to this page with a customer.<br />The customer needs to enter their address and claim the refund.<br />Once a customer claims the refund, you will get a notification and would need to approve and initiate it from your Store > Payouts.",
|
||||
Severity = StatusMessageModel.StatusSeverity.Success
|
||||
});
|
||||
(await ctx.Invoices.FindAsync(new[] { invoice.Id }, cancellationToken))!.CurrentRefundId = ppId;
|
||||
ctx.Refunds.Add(new RefundData()
|
||||
ctx.Refunds.Add(new RefundData
|
||||
{
|
||||
InvoiceDataId = invoice.Id,
|
||||
PullPaymentDataId = ppId
|
||||
});
|
||||
await ctx.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// TODO: Having dedicated UI later on
|
||||
return RedirectToAction(nameof(UIPullPaymentController.ViewPullPayment),
|
||||
"UIPullPayment",
|
||||
|
|
|
@ -6,13 +6,14 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
public enum RefundSteps
|
||||
{
|
||||
SelectPaymentMethod,
|
||||
SelectRate,
|
||||
SelectCustomAmount
|
||||
SelectRate
|
||||
}
|
||||
|
||||
public class RefundModel
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public SelectList AvailablePaymentMethods { get; set; }
|
||||
|
||||
[Display(Name = "Select the payment method used for refund")]
|
||||
public string SelectedPaymentMethod { get; set; }
|
||||
public RefundSteps RefundStep { get; set; }
|
||||
|
|
|
@ -37,9 +37,57 @@
|
|||
const { id, status } = button.dataset
|
||||
changeInvoiceState(id, status)
|
||||
})
|
||||
|
||||
const handleRefundResponse = async response => {
|
||||
const modalBody = document.querySelector('#RefundModal .modal-body')
|
||||
if (response.ok && response.redirected) {
|
||||
window.location = response.url
|
||||
} else if (response.ok) {
|
||||
modalBody.innerHTML = await response.text()
|
||||
} else {
|
||||
modalBody.innerHTML = '<div class="alert alert-danger" role="alert">Failed to load refund options.</div>'
|
||||
}
|
||||
}
|
||||
|
||||
delegate('click', '#IssueRefund', async e => {
|
||||
e.preventDefault()
|
||||
const { href: url } = e.target
|
||||
const response = await fetch(url)
|
||||
await handleRefundResponse(response)
|
||||
})
|
||||
|
||||
delegate('submit', '#RefundForm', async e => {
|
||||
e.preventDefault()
|
||||
const form = e.target
|
||||
const { action: url, method } = form
|
||||
const body = new FormData(form)
|
||||
const response = await fetch(url, { method, body })
|
||||
await handleRefundResponse(response)
|
||||
})
|
||||
</script>
|
||||
}
|
||||
|
||||
@if (Model.CanRefund)
|
||||
{
|
||||
<div id="RefundModal" class="modal fade" tabindex="-1" aria-labelledby="RefundTitle" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="RefundTitle">Issue Refund</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="invoice-details">
|
||||
<div class="sticky-header-setup"></div>
|
||||
<div class="sticky-header d-md-flex align-items-center justify-content-between">
|
||||
|
@ -51,7 +99,7 @@
|
|||
}
|
||||
@if (Model.CanRefund)
|
||||
{
|
||||
<a id="refundlink" class="btn btn-success text-nowrap" asp-action="Refund" asp-route-invoiceId="@Context.GetRouteValue("invoiceId")">Issue Refund</a>
|
||||
<a id="IssueRefund" class="btn btn-success text-nowrap" asp-action="Refund" asp-route-invoiceId="@Model.Id" data-bs-toggle="modal" data-bs-target="#RefundModal">Issue Refund</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
@model RefundModel
|
||||
@{
|
||||
ViewData["Title"] = "Refund";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">@Model.Title</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="post">
|
||||
<input type="hidden" asp-for="RefundStep" value="@Model.RefundStep"/>
|
||||
<input type="hidden" asp-for="Title" value="@Model.Title"/>
|
||||
<input type="hidden" asp-for="RateThenText" value="@Model.RateThenText"/>
|
||||
<input type="hidden" asp-for="CurrentRateText" value="@Model.CurrentRateText"/>
|
||||
<input type="hidden" asp-for="FiatText" value="@Model.FiatText"/>
|
||||
|
||||
@switch (Model.RefundStep)
|
||||
{
|
||||
case RefundSteps.SelectPaymentMethod:
|
||||
<div class="form-group">
|
||||
<label asp-for="SelectedPaymentMethod" class="form-label"></label>
|
||||
<select asp-items="Model.AvailablePaymentMethods" asp-for="SelectedPaymentMethod" class="form-select"></select>
|
||||
<span asp-validation-for="SelectedPaymentMethod" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button id="ok" type="submit" class="btn btn-primary btn-lg w-100">Next</button>
|
||||
</div>
|
||||
break;
|
||||
case RefundSteps.SelectRate:
|
||||
<input type="hidden" asp-for="SelectedPaymentMethod"/>
|
||||
<input type="hidden" asp-for="CryptoAmountThen"/>
|
||||
<input type="hidden" asp-for="FiatAmount"/>
|
||||
<input type="hidden" asp-for="CryptoAmountNow"/>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="RateThenTextRadio" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input"/>
|
||||
<label for="RateThenTextRadio" class="form-check-label">@Model.RateThenText</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">The crypto currency price, at the rate the invoice got paid.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="CurrentRateTextRadio" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input"/>
|
||||
<label for="CurrentRateTextRadio" class="form-check-label">@Model.CurrentRateText</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">The crypto currency price, at the current rate.</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="FiatTextRadio" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input"/>
|
||||
<label for="FiatTextRadio" class="form-check-label">@Model.FiatText</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">The invoice currency, at the rate when the refund will be sent.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="CustomText" asp-for="SelectedRefundOption" type="radio" value="Custom" class="form-check-input"/>
|
||||
<label for="CustomText" class="form-check-label">Custom</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">The specified amount with the specified currency, at the rate when the refund will be sent. </small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<span asp-validation-for="SelectedRefundOption" class="text-danger w-100"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button id="ok" type="submit" class="btn btn-primary btn-lg w-100">Create refund</button>
|
||||
</div>
|
||||
break;
|
||||
case RefundSteps.SelectCustomAmount:
|
||||
|
||||
<input type="hidden" asp-for="SelectedPaymentMethod"/>
|
||||
<input type="hidden" asp-for="CryptoAmountThen"/>
|
||||
<input type="hidden" asp-for="FiatAmount"/>
|
||||
<input type="hidden" asp-for="CryptoAmountNow"/>
|
||||
<input type="hidden" asp-for="SelectedRefundOption"/>
|
||||
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomAmount" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="CustomAmount" type="number" step="any" asp-format="{0}" class="form-control"/>
|
||||
<input asp-for="CustomCurrency" type="text" class="form-control"/>
|
||||
</div>
|
||||
<span asp-validation-for="CustomAmount" class="text-danger w-100"></span>
|
||||
<span asp-validation-for="CustomCurrency" class="text-danger w-100"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button id="ok" type="submit" class="btn btn-primary btn-lg w-100">Next</button>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
87
BTCPayServer/Views/UIInvoice/_RefundModal.cshtml
Normal file
87
BTCPayServer/Views/UIInvoice/_RefundModal.cshtml
Normal file
|
@ -0,0 +1,87 @@
|
|||
@model RefundModel
|
||||
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
|
||||
<form method="post" asp-action="Refund" asp-route-invoiceId="@Context.GetRouteValue("invoiceId")" id="RefundForm">
|
||||
<input type="hidden" asp-for="RefundStep" value="@Model.RefundStep"/>
|
||||
<input type="hidden" asp-for="Title" value="@Model.Title"/>
|
||||
<input type="hidden" asp-for="RateThenText" value="@Model.RateThenText"/>
|
||||
<input type="hidden" asp-for="CurrentRateText" value="@Model.CurrentRateText"/>
|
||||
<input type="hidden" asp-for="FiatText" value="@Model.FiatText"/>
|
||||
|
||||
<h5 class="mb-3">@Model.Title</h5>
|
||||
|
||||
@switch (Model.RefundStep)
|
||||
{
|
||||
case RefundSteps.SelectPaymentMethod:
|
||||
<div class="form-group">
|
||||
<label asp-for="SelectedPaymentMethod" class="form-label"></label>
|
||||
<select asp-items="Model.AvailablePaymentMethods" asp-for="SelectedPaymentMethod" class="form-select"></select>
|
||||
<span asp-validation-for="SelectedPaymentMethod" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button id="ok" type="submit" class="btn btn-primary w-100">Next</button>
|
||||
</div>
|
||||
break;
|
||||
|
||||
case RefundSteps.SelectRate:
|
||||
<input type="hidden" asp-for="SelectedPaymentMethod"/>
|
||||
<input type="hidden" asp-for="CryptoAmountThen"/>
|
||||
<input type="hidden" asp-for="FiatAmount"/>
|
||||
<input type="hidden" asp-for="CryptoAmountNow"/>
|
||||
<style>
|
||||
#CustomOption ~ .form-group { display: none; }
|
||||
#CustomOption:checked ~ .form-group { display: block; }
|
||||
</style>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="RateThenOption" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input"/>
|
||||
<label for="RateThenOption" class="form-check-label">@Model.RateThenText</label>
|
||||
<div class="form-text text-muted">The crypto currency price, at the rate the invoice got paid.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="CurrentRateOption" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input"/>
|
||||
<label for="CurrentRateOption" class="form-check-label">@Model.CurrentRateText</label>
|
||||
<div class="form-text text-muted">The crypto currency price, at the current rate.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="FiatOption" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input"/>
|
||||
<label for="FiatOption" class="form-check-label">@Model.FiatText</label>
|
||||
<div class="form-text text-muted">The invoice currency, at the rate when the refund will be sent.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="CustomOption" asp-for="SelectedRefundOption" type="radio" value="Custom" class="form-check-input"/>
|
||||
<label for="CustomOption" class="form-check-label">Custom amount</label>
|
||||
<div class="form-text text-muted">The specified amount with the specified currency, at the rate when the refund will be sent.</div>
|
||||
<div class="form-group pt-2">
|
||||
<label asp-for="CustomAmount" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input asp-for="CustomAmount" type="number" step="any" asp-format="{0}" class="form-control"/>
|
||||
<input asp-for="CustomCurrency" type="text" class="form-control" currency-selection style="max-width:10ch;"/>
|
||||
</div>
|
||||
<span asp-validation-for="CustomAmount" class="text-danger w-100"></span>
|
||||
<span asp-validation-for="CustomCurrency" class="text-danger w-100"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<span asp-validation-for="SelectedRefundOption" class="text-danger w-100"></span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button id="ok" type="submit" class="btn btn-primary w-100">Create refund</button>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
</form>
|
Loading…
Add table
Reference in a new issue