Public Invoice receipt (#3612)

* Public Invoice receipt

* implement payment,s qr, better ui, and fix invoice bug

* General view updates

* Update admin details link

* Update view

* add missing check

* Refactor

* make payments and qr  shown by default
* move cusotmization options to own ReceiptOptions
* Make sure to sanitize values inside PosData partial

* Refactor

* Make sure that ReceiptOptions for the StoreData is never null, and that values are always set in API

* add receipt link to checkout and add tests

* add receipt  link to lnurl

* Use ReceiptOptions.Merge

* fix lnurl

* fix chrome

* remove i18n parameterization

* Fix swagger

* Update translations

* Fix warning

Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
Andrew Camilleri 2022-07-06 14:14:55 +02:00 committed by GitHub
parent 2a190d579c
commit 3576ebd14f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 785 additions and 199 deletions

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
@ -19,6 +20,48 @@ namespace BTCPayServer.Client.Models
public string Currency { get; set; }
public JObject Metadata { get; set; }
public CheckoutOptions Checkout { get; set; } = new CheckoutOptions();
public ReceiptOptions Receipt { get; set; } = new ReceiptOptions();
public class ReceiptOptions
{
public bool? Enabled { get; set; }
public bool? ShowQR { get; set; }
public bool? ShowPayments { get; set; }
[JsonExtensionData]
public IDictionary<string, JToken> AdditionalData { get; set; }
#nullable enable
/// <summary>
/// Make sure that the return has all values set by order of priority: invoice/store/default
/// </summary>
/// <param name="storeLevelOption"></param>
/// <param name="invoiceLevelOption"></param>
/// <returns></returns>
public static ReceiptOptions Merge(ReceiptOptions? storeLevelOption, ReceiptOptions? invoiceLevelOption)
{
storeLevelOption ??= new ReceiptOptions();
invoiceLevelOption ??= new ReceiptOptions();
var store = JObject.FromObject(storeLevelOption);
var inv = JObject.FromObject(invoiceLevelOption);
var result = JObject.FromObject(CreateDefault());
var mergeSettings = new JsonMergeSettings() { MergeNullValueHandling = MergeNullValueHandling.Ignore };
result.Merge(store, mergeSettings);
result.Merge(inv, mergeSettings);
var options = result.ToObject<ReceiptOptions>()!;
return options;
}
public static ReceiptOptions CreateDefault()
{
return new ReceiptOptions()
{
ShowQR = true,
Enabled = true,
ShowPayments = true
};
}
#nullable restore
}
public class CheckoutOptions
{

View file

@ -58,6 +58,8 @@ namespace BTCPayServer.Client.Models
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;
public bool PayJoinEnabled { get; set; }
public InvoiceData.ReceiptOptions Receipt { get; set; }
[JsonExtensionData]

View file

@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
<PackageReference Include="Selenium.Support" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="101.0.4951.4100" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="103.0.5060.5300" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PrivateAssets>all</PrivateAssets>

View file

@ -131,6 +131,39 @@ namespace BTCPayServer.Tests
}
}
[Fact]
public void CanMergeReceiptOptions()
{
var r = InvoiceDataBase.ReceiptOptions.Merge(null, null);
Assert.True(r?.Enabled);
Assert.True(r?.ShowPayments);
Assert.True(r?.ShowQR);
r = InvoiceDataBase.ReceiptOptions.Merge(new InvoiceDataBase.ReceiptOptions(), null);
Assert.True(r?.Enabled);
Assert.True(r?.ShowPayments);
Assert.True(r?.ShowQR);
r = InvoiceDataBase.ReceiptOptions.Merge(new InvoiceDataBase.ReceiptOptions() { Enabled = false }, null);
Assert.False(r?.Enabled);
Assert.True(r?.ShowPayments);
Assert.True(r?.ShowQR);
r = InvoiceDataBase.ReceiptOptions.Merge(new InvoiceDataBase.ReceiptOptions() { Enabled = false, ShowQR = false }, new InvoiceDataBase.ReceiptOptions() { Enabled = true });
Assert.True(r?.Enabled);
Assert.True(r?.ShowPayments);
Assert.False(r?.ShowQR);
StoreBlob blob = new StoreBlob();
Assert.True(blob.ReceiptOptions.Enabled);
blob = JsonConvert.DeserializeObject<StoreBlob>("{}");
Assert.True(blob.ReceiptOptions.Enabled);
blob = JsonConvert.DeserializeObject<StoreBlob>("{\"receiptOptions\":{\"enabled\": false}}");
Assert.False(blob.ReceiptOptions.Enabled);
blob = JsonConvert.DeserializeObject<StoreBlob>(JsonConvert.SerializeObject(blob));
Assert.False(blob.ReceiptOptions.Enabled);
}
[Fact]
public void CanParsePaymentMethodId()
{

View file

@ -86,9 +86,19 @@ namespace BTCPayServer.Tests
Driver.AssertNoError();
}
public void PayInvoice()
public void PayInvoice(bool mine = false)
{
Driver.FindElement(By.Id("FakePayment")).Click();
if (mine)
{
MineBlockOnInvoiceCheckout();
}
}
public void MineBlockOnInvoiceCheckout()
{
Driver.FindElement(By.CssSelector("#mine-block button")).Click();
}
/// <summary>

View file

@ -14,6 +14,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Payments;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Wallets;
using BTCPayServer.Views.Manage;
using BTCPayServer.Views.Server;
@ -437,6 +438,61 @@ namespace BTCPayServer.Tests
s.Driver.FindElements(By.ClassName("changeInvoiceState"))[0].Click();
TestUtils.Eventually(() => Assert.Contains("Settled (marked)", s.Driver.PageSource));
}
[Fact(Timeout = TestTimeout)]
public async Task CanUseInvoiceReceipts()
{
using var s = CreateSeleniumTester();
await s.StartAsync();
s.RegisterNewUser(true);
s.CreateNewStore();
s.AddDerivationScheme();
s.GoToInvoices();
var i = s.CreateInvoice();
s.GoToInvoiceCheckout(i);
s.PayInvoice(true);
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
});
Assert.Contains(s.Server.PayTester.GetService<CurrencyNameTable>().DisplayFormatCurrency(100, "USD"),
s.Driver.PageSource);
Assert.Contains(i, s.Driver.PageSource);
s.GoToInvoices(s.StoreId);
i = s.CreateInvoice();
s.GoToInvoiceCheckout(i);
var receipturl = s.Driver.Url + "/receipt";
s.Driver.Navigate().GoToUrl(receipturl);
s.Driver.FindElement(By.Id("invoice-unsettled"));
s.GoToInvoices(s.StoreId);
s.GoToInvoiceCheckout(i);
var checkouturi = s.Driver.Url;
s.PayInvoice();
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
Assert.Contains("invoice-processing", s.Driver.PageSource);
});
s.GoToUrl(checkouturi);
s.MineBlockOnInvoiceCheckout();
TestUtils.Eventually(() => s.Driver.FindElement(By.LinkText("View receipt")).Click());
TestUtils.Eventually(() =>
{
s.Driver.Navigate().Refresh();
Assert.DoesNotContain("invoice-unsettled", s.Driver.PageSource);
Assert.DoesNotContain("invoice-processing", s.Driver.PageSource);
});
}
[Fact(Timeout = TestTimeout)]
public async Task CanSetupStoreViaGuide()

View file

@ -57,7 +57,7 @@
<PackageReference Include="Fido2" Version="2.0.1" />
<PackageReference Include="Fido2.AspNet" Version="2.0.1" />
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
<PackageReference Include="LNURL" Version="0.0.22" />
<PackageReference Include="LNURL" Version="0.0.24" />
<PackageReference Include="MailKit" Version="3.3.0" />
<PackageReference Include="McMaster.NETCore.Plugins.Mvc" Version="1.4.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Filter" Version="1.1.2" />

View file

@ -431,7 +431,8 @@ namespace BTCPayServer.Controllers.Greenfield
RedirectAutomatically = entity.RedirectAutomatically,
RequiresRefundEmail = entity.RequiresRefundEmail,
RedirectURL = entity.RedirectURLTemplate
}
},
Receipt = entity.ReceiptOptions
};
}
}

View file

@ -25,13 +25,11 @@ namespace BTCPayServer.Controllers.Greenfield
{
private readonly StoreRepository _storeRepository;
private readonly UserManager<ApplicationUser> _userManager;
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
public GreenfieldStoresController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager, BTCPayNetworkProvider btcPayNetworkProvider)
public GreenfieldStoresController(StoreRepository storeRepository, UserManager<ApplicationUser> userManager)
{
_storeRepository = storeRepository;
_userManager = userManager;
_btcPayNetworkProvider = btcPayNetworkProvider;
}
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
[HttpGet("~/api/v1/stores")]
@ -129,6 +127,7 @@ namespace BTCPayServer.Controllers.Greenfield
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
NetworkFeeMode = storeBlob.NetworkFeeMode,
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi,
LightningPrivateRouteHints = storeBlob.LightningPrivateRouteHints,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
@ -166,6 +165,7 @@ namespace BTCPayServer.Controllers.Greenfield
blob.NetworkFeeMode = restModel.NetworkFeeMode;
blob.DefaultCurrency = restModel.DefaultCurrency;
blob.RequiresRefundEmail = restModel.RequiresRefundEmail;
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback;

View file

@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@ -17,6 +18,7 @@ using BTCPayServer.Filters;
using BTCPayServer.HostedServices;
using BTCPayServer.Models;
using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Models.PaymentRequestViewModels;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Services.Apps;
@ -26,6 +28,7 @@ using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitpayClient;
@ -83,7 +86,7 @@ namespace BTCPayServer.Controllers
}
[HttpGet("invoices/{invoiceId}")]
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[Authorize(Policy = Policies.CanViewInvoices, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> Invoice(string invoiceId)
{
var invoice = (await _InvoiceRepository.GetInvoices(new InvoiceQuery
@ -101,7 +104,8 @@ namespace BTCPayServer.Controllers
var store = await _StoreRepository.FindStore(invoice.StoreId);
if (store == null)
return NotFound();
var receipt = InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, invoice.ReceiptOptions);
var invoiceState = invoice.GetInvoiceState();
var model = new InvoiceDetailsModel
{
@ -133,6 +137,7 @@ namespace BTCPayServer.Controllers
CanRefund = CanRefund(invoiceState),
Refunds = invoice.Refunds,
ShowCheckout = invoice.Status == InvoiceStatusLegacy.New,
ShowReceipt = invoice.Status.ToModernStatus() == InvoiceStatus.Settled && (invoice.ReceiptOptions?.Enabled ?? receipt.Enabled is true),
Deliveries = (await _InvoiceRepository.GetWebhookDeliveries(invoiceId))
.Select(c => new Models.StoreViewModels.DeliveryViewModel(c))
.ToList(),
@ -146,7 +151,85 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet("i/{invoiceId}/receipt")]
public async Task<IActionResult> InvoiceReceipt(string invoiceId)
{
var i = await _InvoiceRepository.GetInvoice(invoiceId);
if (i is null)
return NotFound();
var store = await _StoreRepository.GetStoreByInvoiceId(i.Id);
if (store is null)
return NotFound();
var receipt = InvoiceDataBase.ReceiptOptions.Merge(store.GetStoreBlob().ReceiptOptions, i.ReceiptOptions);
if (receipt.Enabled is not true) return NotFound();
if (i.Status.ToModernStatus() != InvoiceStatus.Settled)
{
return View(new InvoiceReceiptViewModel
{
InvoiceId = i.Id,
OrderId = i.Metadata?.OrderId,
StoreName = store.StoreName,
Status = i.Status.ToModernStatus()
});
}
JToken? receiptData = null;
i.Metadata?.AdditionalData.TryGetValue("receiptData", out receiptData);
return View(new InvoiceReceiptViewModel
{
StoreName = store.StoreName,
Status = i.Status.ToModernStatus(),
Amount = i.Price,
Currency = i.Currency,
Timestamp = i.InvoiceTime,
InvoiceId = i.Id,
OrderId = i.Metadata?.OrderId,
Payments = receipt.ShowPayments is false ? null : i.GetPayments(true).Select(paymentEntity =>
{
var paymentData = paymentEntity.GetCryptoPaymentData();
var paymentMethodId = paymentEntity.GetPaymentMethodId();
if (paymentData is null || paymentMethodId is null)
{
return null;
}
string txId = paymentData.GetPaymentId();
string? link = GetTransactionLink(paymentMethodId, txId);
var paymentMethod = i.GetPaymentMethod(paymentMethodId);
var amount = paymentData.GetValue();
var rate = paymentMethod.Rate;
var paid = (amount - paymentEntity.NetworkFee) * rate;
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
{
Amount = amount,
Paid = paid,
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
PaidFormatted = _CurrencyNameTable.FormatCurrency(paid, i.Currency),
RateFormatted = _CurrencyNameTable.FormatCurrency(rate, i.Currency),
PaymentMethod = paymentMethodId.ToPrettyString(),
Link = link,
Id = txId,
Destination = paymentData.GetDestination()
};
})
.Where(payment => payment != null)
.ToList(),
ReceiptOptions = receipt,
AdditionalData = receiptData is null
? new Dictionary<string, object>()
: PosDataParser.ParsePosData(receiptData.ToString())
});
}
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
{
var network = _NetworkProvider.GetNetwork(paymentMethodId.CryptoCode);
return network == null ? null : paymentMethodId.PaymentType.GetTransactionLink(network, txId);
}
bool CanRefund(InvoiceState invoiceState)
{
return invoiceState.Status == InvoiceStatusLegacy.Confirmed ||
@ -632,6 +715,15 @@ namespace BTCPayServer.Controllers
}
lang ??= storeBlob.DefaultLang;
var receiptEnabled = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, invoice.ReceiptOptions).Enabled is true;
var receiptUrl = receiptEnabled? _linkGenerator.GetUriByAction(
nameof(UIInvoiceController.InvoiceReceipt),
"UIInvoice",
new {invoiceId},
Request.Scheme,
Request.Host,
Request.PathBase) : null;
var model = new PaymentModel
{
Activated = paymentMethodDetails.Activated,
@ -657,7 +749,8 @@ namespace BTCPayServer.Controllers
MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes,
ItemDesc = invoice.Metadata.ItemDesc,
Rate = ExchangeRate(paymentMethod),
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? "/",
MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? receiptUrl ?? "/",
ReceiptLink = receiptUrl,
RedirectAutomatically = invoice.RedirectAutomatically,
StoreName = store.StoreName,
TxCount = accounting.TxRequired,
@ -1079,25 +1172,7 @@ namespace BTCPayServer.Controllers
var jObject = JObject.Parse(posData);
foreach (var item in jObject)
{
switch (item.Value?.Type)
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count; i++)
{
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.TryAdd(item.Key, ParsePosData(item.Value.ToString()));
break;
case null:
break;
default:
result.TryAdd(item.Key, item.Value.ToString());
break;
}
ParsePosDataItem(item, ref result);
}
}
catch
@ -1106,6 +1181,29 @@ namespace BTCPayServer.Controllers
}
return result;
}
public static void ParsePosDataItem(KeyValuePair<string, JToken?> item, ref Dictionary<string, object> result)
{
switch (item.Value?.Type)
{
case JTokenType.Array:
var items = item.Value.AsEnumerable().ToList();
for (var i = 0; i < items.Count; i++)
{
result.TryAdd($"{item.Key}[{i}]", ParsePosData(items[i].ToString()));
}
break;
case JTokenType.Object:
result.TryAdd(item.Key, ParsePosData(item.Value.ToString()));
break;
case null:
break;
default:
result.TryAdd(item.Key, item.Value.ToString());
break;
}
}
}
}
}

View file

@ -22,6 +22,7 @@ using BTCPayServer.Services.Stores;
using BTCPayServer.Validation;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using NBitpayClient;
using BitpayCreateInvoiceRequest = BTCPayServer.Models.BitpayCreateInvoiceRequest;
using StoreData = BTCPayServer.Data.StoreData;
@ -44,6 +45,7 @@ namespace BTCPayServer.Controllers
private readonly LanguageService _languageService;
private readonly ExplorerClientProvider _ExplorerClients;
private readonly UIWalletsController _walletsController;
private readonly LinkGenerator _linkGenerator;
public WebhookSender WebhookNotificationManager { get; }
@ -62,7 +64,8 @@ namespace BTCPayServer.Controllers
WebhookSender webhookNotificationManager,
LanguageService languageService,
ExplorerClientProvider explorerClients,
UIWalletsController walletsController)
UIWalletsController walletsController,
LinkGenerator linkGenerator)
{
_CurrencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
_StoreRepository = storeRepository ?? throw new ArgumentNullException(nameof(storeRepository));
@ -78,6 +81,7 @@ namespace BTCPayServer.Controllers
_languageService = languageService;
this._ExplorerClients = explorerClients;
_walletsController = walletsController;
_linkGenerator = linkGenerator;
}
@ -159,6 +163,7 @@ namespace BTCPayServer.Controllers
entity.PaymentTolerance = storeBlob.PaymentTolerance;
entity.DefaultPaymentMethod = invoice.DefaultPaymentMethod;
entity.RequiresRefundEmail = invoice.RequiresRefundEmail;
return await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken);
}
@ -169,6 +174,7 @@ namespace BTCPayServer.Controllers
entity.ServerUrl = serverUrl;
entity.ExpirationTime = entity.InvoiceTime + (invoice.Checkout.Expiration ?? storeBlob.InvoiceExpiration);
entity.MonitoringExpiration = entity.ExpirationTime + (invoice.Checkout.Monitoring ?? storeBlob.MonitoringExpiration);
entity.ReceiptOptions = invoice.Receipt ?? new InvoiceDataBase.ReceiptOptions();
if (invoice.Metadata != null)
entity.Metadata = InvoiceMetadata.FromJObject(invoice.Metadata);
invoice.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();

View file

@ -342,7 +342,8 @@ namespace BTCPayServer
return NotFound("Store not found");
}
currencyCode ??= store.GetStoreBlob().DefaultCurrency ?? cryptoCode;
var storeBlob = store.GetStoreBlob();
currencyCode ??= storeBlob.DefaultCurrency ?? cryptoCode;
var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay);
var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike);
var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider);
@ -429,7 +430,6 @@ namespace BTCPayServer
{
lnurlMetadata.Add(new[] {"text/identifier", lnAddress});
}
return Ok(new LNURLPayRequest
{
Tag = "payRequest",
@ -513,6 +513,18 @@ namespace BTCPayServer
return BadRequest(new LNUrlStatusResponse {Status = "ERROR", Reason = "Amount is out of bounds."});
}
LNURLPayRequest.LNURLPayRequestCallbackResponse.ILNURLPayRequestSuccessAction successAction = null;
if ((i.ReceiptOptions?.Enabled ??blob.ReceiptOptions.Enabled ) is true)
{
successAction =
new LNURLPayRequest.LNURLPayRequestCallbackResponse.LNURLPayRequestSuccessActionUrl()
{
Tag = "url",
Description = "Thank you for your purchase. Here is your receipt",
Url = _linkGenerator.GetUriByAction(HttpContext, "InvoiceReceipt", "UIInvoice", new { invoiceId})
};
}
if (amount.HasValue && string.IsNullOrEmpty(paymentMethodDetails.BOLT11) ||
paymentMethodDetails.GeneratedBoltAmount != amount)
{
@ -573,7 +585,8 @@ namespace BTCPayServer
paymentMethodDetails, pmi));
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
{
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11,
SuccessAction = successAction
});
}
@ -588,7 +601,8 @@ namespace BTCPayServer
return Ok(new LNURLPayRequest.LNURLPayRequestCallbackResponse
{
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11
Disposable = true, Routes = Array.Empty<string>(), Pr = paymentMethodDetails.BOLT11,
SuccessAction = successAction
});
}

View file

@ -385,6 +385,7 @@ namespace BTCPayServer.Controllers
vm.CustomCSS = storeBlob.CustomCSS;
vm.CustomLogo = storeBlob.CustomLogo;
vm.HtmlTitle = storeBlob.HtmlTitle;
vm.ReceiptOptions = CheckoutAppearanceViewModel.ReceiptOptionsViewModel.Create(storeBlob.ReceiptOptions);
vm.AutoDetectLanguage = storeBlob.AutoDetectLanguage;
vm.SetLanguages(_LangService, storeBlob.DefaultLang);
@ -496,6 +497,7 @@ namespace BTCPayServer.Controllers
blob.RequiresRefundEmail = model.RequiresRefundEmail;
blob.LazyPaymentMethods = model.LazyPaymentMethods;
blob.RedirectAutomatically = model.RedirectAutomatically;
blob.ReceiptOptions = model.ReceiptOptions.ToDTO();
blob.CustomLogo = model.CustomLogo;
blob.CustomCSS = model.CustomCSS;
blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle;

View file

@ -30,6 +30,7 @@ namespace BTCPayServer.Data
ShowRecommendedFee = true;
RecommendedFeeBlockTarget = 1;
PaymentMethodCriteria = new List<PaymentMethodCriteria>();
ReceiptOptions = InvoiceDataBase.ReceiptOptions.CreateDefault();
}
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
@ -104,7 +105,10 @@ namespace BTCPayServer.Data
public bool AutoDetectLanguage { get; set; }
public bool RateScripting { get; set; }
#nullable enable
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public InvoiceDataBase.ReceiptOptions ReceiptOptions { get; set; }
#nullable restore
public string RateScript { get; set; }
public bool AnyoneCanInvoice { get; set; }

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.Data;
using BTCPayServer.Data;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services.Invoices;
@ -133,5 +134,6 @@ namespace BTCPayServer.Models.InvoicingModels
public bool CanMarkInvalid { get; set; }
public bool CanMarkStatus => CanMarkSettled || CanMarkInvalid;
public List<RefundData> Refunds { get; set; }
public bool ShowReceipt { get; set; }
}
}

View file

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.Models;
using BTCPayServer.Models.PaymentRequestViewModels;
using static BTCPayServer.Client.Models.InvoiceDataBase;
namespace BTCPayServer.Models.InvoicingModels
{
public class InvoiceReceiptViewModel
{
public InvoiceStatus Status { get; set; }
public string InvoiceId { get; set; }
public string OrderId { get; set; }
public string Currency { get; set; }
public string StoreName { get; set; }
public decimal Amount { get; set; }
public DateTimeOffset Timestamp { get; set; }
public Dictionary<string, object> AdditionalData { get; set; }
public ReceiptOptions ReceiptOptions { get; set; }
public List<ViewPaymentRequestViewModel.PaymentRequestInvoicePayment> Payments { get; set; }
}
}

View file

@ -68,5 +68,6 @@ namespace BTCPayServer.Models.InvoicingModels
public bool RedirectAutomatically { get; set; }
public bool Activated { get; set; }
public string InvoiceCurrency { get; set; }
public string? ReceiptLink { get; set; }
}
}

View file

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc.Rendering;
using Newtonsoft.Json.Linq;
namespace BTCPayServer.Models.StoreViewModels
{
@ -47,6 +48,26 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Custom HTML title to display on Checkout page")]
public string HtmlTitle { get; set; }
public class ReceiptOptionsViewModel
{
public static ReceiptOptionsViewModel Create(Client.Models.InvoiceDataBase.ReceiptOptions opts)
{
return JObject.FromObject(opts).ToObject<ReceiptOptionsViewModel>();
}
[Display(Name = "Enable public receipt page for settled invoices")]
public bool Enabled { get; set; }
[Display(Name = "Show the QR code of the receipt in the public receipt page")]
public bool ShowQR { get; set; }
[Display(Name = "Show the payment list in the public receipt page")]
public bool ShowPayments { get; set; }
public Client.Models.InvoiceDataBase.ReceiptOptions ToDTO()
{
return JObject.FromObject(this).ToObject<Client.Models.InvoiceDataBase.ReceiptOptions>();
}
}
public ReceiptOptionsViewModel ReceiptOptions { get; set; } = ReceiptOptionsViewModel.Create(Client.Models.InvoiceDataBase.ReceiptOptions.CreateDefault());
public List<PaymentMethodCriteriaViewModel> PaymentMethodCriteria { get; set; }
}

View file

@ -442,6 +442,9 @@ namespace BTCPayServer.Services.Invoices
public InvoiceType Type { get; set; }
public List<RefundData> Refunds { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public InvoiceDataBase.ReceiptOptions ReceiptOptions { get; set; }
public bool IsExpired()
{

View file

@ -204,8 +204,12 @@
</div>
</div>
<div class="success-message">{{$t("This invoice has been paid")}}</div>
<a class="action-button" :href="srvModel.merchantRefLink" v-show="!isModal">
<span v-html="$t('Return to StoreName', srvModel)"></span>
<a class="action-button" :href="srvModel.merchantRefLink" v-if="!isModal && srvModel.merchantRefLink">
<span v-if="srvModel.receiptLink != srvModel.merchantRefLink" v-html="$t('Return to StoreName', srvModel)"></span>
<span v-else v-html="$t('View receipt')"></span>
</a>
<a class="action-button" :href="srvModel.receiptLink" :target="isModal?'_blank':'_top'" v-if="srvModel.receiptLink && (srvModel.merchantRefLink != srvModel.receiptLink) || isModal">
<span v-html="$t('View receipt')"></span>
</a>
<button class="action-button close-action" v-show="isModal" v-on:click="close">
<span v-html="$t('Close')"></span>

View file

@ -90,20 +90,24 @@
<div class="invoice-details">
<div class="sticky-header-setup"></div>
<div class="sticky-header d-md-flex align-items-center justify-content-between">
<h2 class="mb-0">@ViewData["Title"]</h2>
<div class="d-flex gap-3 mt-3 mt-md-0">
<div class="sticky-header d-flex flex-wrap gap-3 align-items-center justify-content-between">
<h2 class="mb-0 text-break">@ViewData["Title"]</h2>
<div class="d-flex flex-wrap gap-3">
@if (Model.ShowCheckout)
{
<a asp-action="Checkout" class="invoice-checkout-link btn btn-primary text-nowrap" asp-route-invoiceId="@Model.Id">Checkout</a>
}
@if (Model.ShowReceipt)
{
<a asp-action="InvoiceReceipt" asp-route-invoiceId="@Model.Id" id="Receipt" class="btn btn-secondary" target="InvoiceReceipt-@Model.Id">Receipt</a>
}
@if (Model.CanRefund)
{
<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>
<a asp-action="Refund" asp-route-invoiceId="@Model.Id" id="IssueRefund" class="btn btn-success text-nowrap" data-bs-toggle="modal" data-bs-target="#RefundModal">Issue Refund</a>
}
else
{
<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>Issue refund</button>
<button 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>Issue refund</button>
}
<form asp-action="ToggleArchive" asp-route-invoiceId="@Model.Id" method="post">
<button type="submit" class="btn btn-secondary" id="btn-archive-toggle">

View file

@ -0,0 +1,170 @@
@model BTCPayServer.Models.InvoicingModels.InvoiceReceiptViewModel
@using BTCPayServer.Client
@using BTCPayServer.Client.Models
@using BTCPayServer.Services.Rates
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Mvc.TagHelpers
@inject BTCPayServer.Services.BTCPayServerEnvironment env
@inject BTCPayServer.Services.ThemeSettings Theme
@inject CurrencyNameTable CurrencyNameTable
@{
Layout = null;
ViewData["Title"] = $"Receipt from {Model.StoreName}";
var isProcessing = Model.Status == InvoiceStatus.Processing;
var isSettled = Model.Status == InvoiceStatus.Settled;
}
<!DOCTYPE html>
<html lang="en" @(env.IsDeveloping ? " data-devenv" : "")>
<head>
<partial name="LayoutHead" />
<meta name="robots" content="noindex,nofollow">
@if (isProcessing)
{
<script type="text/javascript">
setTimeout(() => { window.location.reload(); }, 10000);
</script>
}
<style>
#InvoiceSummary { gap: var(--btcpay-space-l); }
#posData td > table:last-child { margin-bottom: 0 !important; }
#posData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
</style>
</head>
<body>
<div class="min-vh-100 d-flex flex-column">
<main class="flex-grow-1 py-5">
<div class="container" style="max-width:720px;">
<partial name="_StatusMessage" model="@(new ViewDataDictionary(ViewData) { { "Margin", "mb-4" } })"/>
<div class="d-flex flex-column justify-content-center gap-4">
<h1 class="h3 text-center">@ViewData["Title"]</h1>
<div id="InvoiceSummary" class="bg-tile p-3 p-sm-4 rounded d-flex flex-wrap align-items-center">
@if (isProcessing)
{
<div class="lead text-center text-muted py-5 px-4 fw-semibold" id="invoice-processing">
The invoice has detected a payment but is still waiting to be settled.
</div>
}
else if (!isSettled)
{
<div class="lead text-center text-muted py-5 px-4 fw-semibold" id="invoice-unsettled">
The invoice is not settled.
</div>
}
else
{
if (Model.ReceiptOptions.ShowQR is true)
{
<div class="mx-auto">
<vc:qr-code data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
</div>
}
<dl class="d-flex flex-column gap-4 mb-0 flex-fill">
<div class="d-flex flex-column">
<div class="d-flex align-items-center justify-content-between">
<button type="button" class="btn btn-link p-0 d-print-none fw-semibold order-1" onclick="window.print()">
Print
</button>
<dd class="text-muted mb-0 fw-semibold">Amount Paid</dd>
</div>
<dt class="fs-2 mb-0 text-nowrap fw-semibold">@CurrencyNameTable.DisplayFormatCurrency(Model.Amount, Model.Currency)</dt>
</div>
<div class="d-flex flex-column">
<dd class="text-muted mb-0 fw-semibold">Date</dd>
<dt class="fs-5 mb-0 text-nowrap fw-semibold">@Model.Timestamp.ToBrowserDate()</dt>
</div>
@if (!string.IsNullOrEmpty(Model.OrderId))
{
<div class="d-flex flex-column">
<dd class="text-muted mb-0 fw-semibold">Order ID</dd>
<dt class="fs-5 mb-0 text-break fw-semibold">@Model.OrderId</dt>
</div>
}
</dl>
}
</div>
@if (isProcessing)
{
<small class="d-block text-muted text-center px-4">This page will refresh periodically until the invoice is settled.</small>
}
else if (isSettled)
{
if (Model.Payments?.Any() is true)
{
<div id="PaymentDetails" class="bg-tile p-3 p-sm-4 rounded">
<h2 class="h4 mb-3 d-print-none">Payment Details</h2>
<div class="table-responsive my-0">
<table class="table my-0">
<tr>
<th class="fw-normal text-secondary">Destination</th>
<th class="fw-normal text-secondary">Received</th>
<th class="fw-normal text-secondary text-end">Paid</th>
<th class="fw-normal text-secondary text-end">Rate</th>
<th class="fw-normal text-secondary text-end">Payment</th>
</tr>
@foreach (var payment in Model.Payments)
{
<tr class="table-borderless table-light">
<td class="text-break">
<code>@payment.Destination</code>
</td>
<td>@payment.ReceivedDate.ToString("g")</td>
<td class="text-end">@payment.PaidFormatted</td>
<td class="text-end">@payment.RateFormatted</td>
<td class="text-end text-nowrap">@payment.Amount @payment.PaymentMethod</td>
</tr>
<tr class="table-borderless table-light">
<td class="fw-normal" colspan="5">
<span class="text-secondary">Transaction Id:</span>
@if (!string.IsNullOrEmpty(payment.Link))
{
<a href="@payment.Link" class="text-print-default text-break" rel="noreferrer noopener" target="_blank">@payment.Id</a>
}
else
{
<span class="text-break">@payment.Id</span>
}
</td>
</tr>
}
</table>
</div>
</div>
}
if (Model.AdditionalData?.Any() is true)
{
<div id="AdditionalData" class="bg-tile p-3 p-sm-4 rounded">
<h2 class="h4 mb-3 d-print-none">Additional Data</h2>
<div class="table-responsive my-0">
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
</div>
</div>
}
}
</div>
</div>
</main>
<footer class="pt-2 pb-4 d-print-none">
<p class="container text-center" permission="@Policies.CanViewInvoices">
<a asp-action="Invoice" asp-route-invoiceId="@Model.InvoiceId">
Admin details
</a>
</p>
<div class="container d-flex flex-wrap align-items-center justify-content-center">
<span class="text-muted mx-2">
Powered by <a href="https://btcpayserver.org" target="_blank" rel="noreferrer noopener">BTCPay Server</a>
</span>
@if (!Theme.CustomTheme)
{
<vc:theme-switch css-class="text-muted mx-2" responsive="none"/>
}
</div>
</footer>
</div>
<partial name="LayoutFoot"/>
</body>
</html>

View file

@ -1,6 +1,6 @@
@model (Dictionary<string, object> Items, int Level)
<table class="table table-hover table-responsive-md removetopborder">
<table class="table table-hover my-0">
@foreach (var (key, value) in Model.Items)
{
<tr>
@ -8,17 +8,17 @@
{
if (!string.IsNullOrEmpty(key))
{
<th class="w-150px">@key</th>
<th class="w-150px">@Safe.Raw(key)</th>
}
<td>
@if (str.StartsWith("http"))
{
<a href="@str" target="_blank" rel="noreferrer noopener">@str</a>
}
else
{
@value
}
@if (Uri.IsWellFormedUriString(str, UriKind.RelativeOrAbsolute))
{
<a href="@Safe.Raw(str)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str)</a>
}
else
{
@Safe.Raw(value?.ToString())
}
</td>
}
else if (value is Dictionary<string, object>subItems)
@ -26,22 +26,22 @@
@* This is the array case *@
if (subItems.Count == 1 && subItems.First().Value is string str2)
{
<th class="w-150px">@key</th>
<th class="w-150px">@Safe.Raw(key)</th>
<td>
@if (str2.StartsWith("http"))
@if (Uri.IsWellFormedUriString(str2, UriKind.RelativeOrAbsolute))
{
<a href="@str2" target="_blank" rel="noreferrer noopener">@str2</a>
<a href="@Safe.Raw(str2)" target="_blank" rel="noreferrer noopener">@Safe.Raw(str2)</a>
}
else
{
@subItems.First().Value
@Safe.Raw(subItems.First().Value?.ToString())
}
</td>
}
else
{
<td colspan="2">
@Html.Raw($"<h{Model.Level + 3} class='mt-3'>{key}</h{Model.Level + 3}>")
<td colspan="2" >
@Safe.Raw($"<h{Model.Level + 3} class=\"mt-4 mb-3\">{key}</h{Model.Level + 3}>")
<partial name="PosData" model="(subItems, Model.Level + 1)"/>
</td>
}

View file

@ -238,128 +238,129 @@
<div class="col">
<div class="bg-tile h-100 m-0 p-3 p-sm-5 rounded">
<h2 class="h4 mb-0">Payment History</h2>
<div class="table-responsive">
<noscript>
@if (Model.Invoices == null || !Model.Invoices.Any())
<noscript>
@if (Model.Invoices == null || !Model.Invoices.Any())
{
<p class="text-muted mt-3 mb-0">No payments made yet.</p>
}
else
{
@foreach (var invoice in Model.Invoices)
{
<p class="text-muted mt-3 mb-0">No payments made yet.</p>
}
else
{
@foreach (var invoice in Model.Invoices)
{
<div class="table-responsive">
<table class="invoice table">
<thead>
<tr class="table-borderless">
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
<th class="fw-normal text-secondary w-175px">Expiry</th>
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
<th class="fw-normal text-secondary text-end w-125px"></th>
<th class="fw-normal text-secondary text-end">Status</th>
</tr>
<tr class="table-borderless">
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
<th class="fw-normal text-secondary w-175px">Expiry</th>
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
<th class="fw-normal text-secondary text-end w-125px"></th>
<th class="fw-normal text-secondary text-end">Status</th>
</tr>
</thead>
<tbody>
<tr class="table-borderless table-light">
<td>@invoice.Id</td>
<td>@invoice.ExpiryDate.ToString("g")</td>
<td class="text-end">@invoice.AmountFormatted</td>
<td class="text-end"></td>
<td class="text-end text-print-default">
<span class="badge bg-@StatusClass(invoice.State)">@invoice.StateFormatted</span>
</td>
</tr>
@if (invoice.Payments != null && invoice.Payments.Any())
{
<tr class="table-borderless table-light">
<td>@invoice.Id</td>
<td>@invoice.ExpiryDate.ToString("g")</td>
<td class="text-end">@invoice.AmountFormatted</td>
<td class="text-end"></td>
<td class="text-end text-print-default">
<span class="badge bg-@StatusClass(invoice.State)">@invoice.StateFormatted</span>
</td>
<th class="fw-normal text-secondary">Destination</th>
<th class="fw-normal text-secondary">Received</th>
<th class="fw-normal text-secondary text-end">Paid</th>
<th class="fw-normal text-secondary text-end">Rate</th>
<th class="fw-normal text-secondary text-end">Payment</th>
</tr>
@if (invoice.Payments != null && invoice.Payments.Any())
@foreach (var payment in invoice.Payments)
{
<tr class="table-borderless table-light">
<th class="fw-normal text-secondary">Destination</th>
<th class="fw-normal text-secondary">Received</th>
<th class="fw-normal text-secondary text-end">Paid</th>
<th class="fw-normal text-secondary text-end">Rate</th>
<th class="fw-normal text-secondary text-end">Payment</th>
<td class="text-break"><code>@payment.Destination</code></td>
<td>@payment.ReceivedDate.ToString("g")</td>
<td class="text-end">@payment.PaidFormatted</td>
<td class="text-end">@payment.RateFormatted</td>
<td class="text-end text-nowrap">@payment.Amount @payment.PaymentMethod</td>
</tr>
<tr class="table-borderless table-light">
<td class="fw-normal" colspan="5">
<span class="text-secondary">Transaction Id:</span>
@if (!string.IsNullOrEmpty(payment.Link))
{
<a href="@payment.Link" class="text-print-default text-break" rel="noreferrer noopener" target="_blank">@payment.Id</a>
}
else
{
<span class="text-break">@payment.Id</span>
}
</td>
</tr>
@foreach (var payment in invoice.Payments)
{
<tr class="table-borderless table-light">
<td class="text-break"><code>@payment.Destination</code></td>
<td>@payment.ReceivedDate.ToString("g")</td>
<td class="text-end">@payment.PaidFormatted</td>
<td class="text-end">@payment.RateFormatted</td>
<td class="text-end text-nowrap">@payment.Amount @payment.PaymentMethod</td>
</tr>
<tr class="table-borderless table-light">
<td class="fw-normal" colspan="5">
<span class="text-secondary">Transaction Id:</span>
@if (!string.IsNullOrEmpty(payment.Link))
{
<a href="@payment.Link" class="text-print-default text-break" rel="noreferrer noopener" target="_blank">@payment.Id</a>
}
else
{
<span class="text-break">@payment.Id</span>
}
</td>
</tr>
}
}
}
</tbody>
</table>
}
</div>
}
</noscript>
<template v-if="!srvModel.invoices || srvModel.invoices.length == 0">
<p class="text-muted mt-3 mb-0">No payments made yet.</p>
</template>
<template v-else>
}
</noscript>
<template v-if="!srvModel.invoices || srvModel.invoices.length == 0">
<p class="text-muted mt-3 mb-0">No payments made yet.</p>
</template>
<template v-else>
<div class="table-responsive">
<table v-for="invoice of srvModel.invoices" :key="invoice.id" class="invoice table">
<thead>
<tr class="table-borderless">
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
<th class="fw-normal text-secondary w-175px">Expiry</th>
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
<th class="fw-normal text-secondary text-end w-125px"></th>
<th class="fw-normal text-secondary text-end">Status</th>
</tr>
<tr class="table-borderless">
<th class="fw-normal text-secondary w-350px" scope="col">Invoice Id</th>
<th class="fw-normal text-secondary w-175px">Expiry</th>
<th class="fw-normal text-secondary text-end w-125px">Amount</th>
<th class="fw-normal text-secondary text-end w-125px"></th>
<th class="fw-normal text-secondary text-end">Status</th>
</tr>
</thead>
<tbody>
<tr class="table-borderless table-light">
<td>{{invoice.id}}</td>
<td v-text="formatDate(invoice.expiryDate)"></td>
<td class="text-end">{{invoice.amountFormatted}}</td>
<td class="text-end"></td>
<td class="text-end text-print-default">
<span class="badge" :class="`bg-${statusClass(invoice.stateFormatted)}`">{{invoice.stateFormatted}}</span>
</td>
</tr>
<template v-if="invoice.payments && invoice.payments.length > 0">
<tr class="table-borderless table-light">
<td>{{invoice.id}}</td>
<td v-text="formatDate(invoice.expiryDate)"></td>
<td class="text-end">{{invoice.amountFormatted}}</td>
<td class="text-end"></td>
<td class="text-end text-print-default">
<span class="badge" :class="`bg-${statusClass(invoice.stateFormatted)}`">{{invoice.stateFormatted}}</span>
</td>
<th class="fw-normal text-secondary">Destination</th>
<th class="fw-normal text-secondary">Received</th>
<th class="fw-normal text-secondary text-end">Paid</th>
<th class="fw-normal text-secondary text-end">Rate</th>
<th class="fw-normal text-secondary text-end">Payment</th>
</tr>
<template v-if="invoice.payments && invoice.payments.length > 0">
<template v-for="payment of invoice.payments">
<tr class="table-borderless table-light">
<th class="fw-normal text-secondary">Destination</th>
<th class="fw-normal text-secondary">Received</th>
<th class="fw-normal text-secondary text-end">Paid</th>
<th class="fw-normal text-secondary text-end">Rate</th>
<th class="fw-normal text-secondary text-end">Payment</th>
<td class="text-break"><code>{{payment.destination}}</code></td>
<td v-text="formatDate(payment.receivedDate)"></td>
<td class="text-end">{{payment.paidFormatted}}</td>
<td class="text-end">{{payment.rateFormatted}}</td>
<td class="text-end text-nowrap">{{payment.amount.noExponents()}} {{payment.paymentMethod}}</td>
</tr>
<tr class="table-borderless table-light">
<td class="fw-normal" colspan="5">
<span class="text-secondary">Transaction Id:</span>
<a v-if="payment.link" :href="payment.link" class="text-print-default" target="_blank" rel="noreferrer noopener">{{payment.id}}</a>
<span v-else>{{payment.id}}</span>
</td>
</tr>
<template v-for="payment of invoice.payments">
<tr class="table-borderless table-light">
<td class="text-break"><code>{{payment.destination}}</code></td>
<td v-text="formatDate(payment.receivedDate)"></td>
<td class="text-end">{{payment.paidFormatted}}</td>
<td class="text-end">{{payment.rateFormatted}}</td>
<td class="text-end text-nowrap">{{payment.amount.noExponents()}} {{payment.paymentMethod}}</td>
</tr>
<tr class="table-borderless table-light">
<td class="fw-normal" colspan="5">
<span class="text-secondary">Transaction Id:</span>
<a v-if="payment.link" :href="payment.link" class="text-print-default" target="_blank" rel="noreferrer noopener">{{payment.id}}</a>
<span v-else>{{payment.id}}</span>
</td>
</tr>
</template>
</template>
</template>
</tbody>
</table>
</template>
</div>
</div>
</template>
</div>
</div>
</div>

View file

@ -55,6 +55,19 @@
<input asp-for="RedirectAutomatically" type="checkbox" class="form-check-input" />
<label asp-for="RedirectAutomatically" class="form-check-label"></label>
</div>
<h3 class="mb-3">Public receipt</h3>
<div class="form-check my-1">
<input asp-for="ReceiptOptions.Enabled" type="checkbox" class="form-check-input" />
<label asp-for="ReceiptOptions.Enabled" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="ReceiptOptions.ShowPayments" type="checkbox" class="form-check-input" />
<label asp-for="ReceiptOptions.ShowPayments" class="form-check-label"></label>
</div>
<div class="form-check my-1">
<input asp-for="ReceiptOptions.ShowQR" type="checkbox" class="form-check-input" />
<label asp-for="ReceiptOptions.ShowQR" class="form-check-label"></label>
</div>
<h3 class="mt-5 mb-3">Language</h3>

View file

@ -7,7 +7,7 @@
<h3 class="mb-0">@ViewData["Title"]</h3>
<div class="row">
<div class="col-md-4">
<table class="table table-hover removetopborder">
<table class="table table-hover">
<tr>
<th>Label</th>
<td class="text-end">@Model.Label</td>

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "በ Changelly ይክፈሉ",
"Close": "ዝጋ",
"NotPaid_ExtraTransaction": "ደረሰኙ ሙሉ በሙሉ አልተከፈለውም. እባክዎ የገንዘብ መጠን ለመሸፈን ሌላ ግብይት ይላኩ",
"Recommended_Fee": "የሚመከር ክፍያ ፦ {{feeRate}} sat/byte"
"Recommended_Fee": "የሚመከር ክፍያ ፦ {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "ادفع عن طريق خدمة Changelly",
"Close": "اغلاق",
"NotPaid_ExtraTransaction": "لم يتم دفع الفاتورة بشكل كامل. من فضلك ارسل تحويلة اخري تغطي الثمن المتبقي.",
"Recommended_Fee": "العمولة المنصوح بها: {{feeRate}} sat/byte"
"Recommended_Fee": "العمولة المنصوح بها: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Changelly vasitəsilə ödə",
"Close": "Qapat",
"NotPaid_ExtraTransaction": "Faktura tam ödənilməyib. Lütfən məbləği tam ödəmək üçün daha bir tranzaksiya həyata keçirin.",
"Recommended_Fee": "Tövsiyə edilən komissiya: {{feeRate}} sat/byte"
"Recommended_Fee": "Tövsiyə edilən komissiya: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Плати с Changelly",
"Close": "Затвори",
"NotPaid_ExtraTransaction": "Фактурата не а платена изцяло. Моля пратете остатъка в допълнителна транзакция. ",
"Recommended_Fee": "Препоръчена тарифа: {{feeRate}} сат/байт (sat/byte)"
"Recommended_Fee": "Препоръчена тарифа: {{feeRate}} сат/байт (sat/byte)",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Plati sa Changelly",
"Close": "Zatvori",
"NotPaid_ExtraTransaction": "Račun nije plaćen u cijelosti. Pošaljite još jednu transakciju kako biste pokrili iznos koji dospijeva.",
"Recommended_Fee": "Preporučena naknada: {{feeRate}} sat / bajt"
"Recommended_Fee": "Preporučena naknada: {{feeRate}} sat / bajt",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Pagueu amb Changelly",
"Close": "Tancar",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Zaplatit přes Changelly",
"Close": "Zavřít",
"NotPaid_ExtraTransaction": "Faktura nebyla uhrazena v plné výši. Zašlete prosím další transakci na úhradu dlužné částky.",
"Recommended_Fee": "Doporučený poplatek: {{feeRate}} sat/bajt"
"Recommended_Fee": "Doporučený poplatek: {{feeRate}} sat/bajt",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Betal med Changelly",
"Close": "Luk",
"NotPaid_ExtraTransaction": "Fakturaen er ikke fuldt ud betalt. Venligst send endnu en transaktion for at dække for den manglende mængde.",
"Recommended_Fee": "Anbefalt gebyr: {{feeRate}} sat/byte"
"Recommended_Fee": "Anbefalt gebyr: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Zahlen mit Changelly",
"Close": "Schließen",
"NotPaid_ExtraTransaction": "Die Rechnung wurde nicht vollständig bezahlt. Bitte senden Sie den fehlenden Betrag, um die Rechnung zu begleichen.",
"Recommended_Fee": "Empfohlene Netzwerk Gebührenrate: {{feeRate}} sat/byte"
"Recommended_Fee": "Empfohlene Netzwerk Gebührenrate: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Πληρώστε με Changelly",
"Close": "Κλείσιμο",
"NotPaid_ExtraTransaction": "Το τιμολόγιο δεν έχει πληρωθεί πλήρως. Στείλτε άλλη συναλλαγή για να καλύψετε το οφειλόμενο ποσό.",
"Recommended_Fee": "Προτεινόμενη χρέωση: {{feeRate}} sat/byte"
"Recommended_Fee": "Προτεινόμενη χρέωση: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Pay with Changelly",
"Close": "Close",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Pagar con Changelly",
"Close": "Cerrar",
"NotPaid_ExtraTransaction": "La factura no ha sido pagada en su totalidad. Por favor envía otra transacción para cubrir el monto faltante.",
"Recommended_Fee": "Comisión recomendada: {{feeRate}} sat/byte"
"Recommended_Fee": "Comisión recomendada: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Maksa käyttäen Changelly:ä",
"Close": "Sulje",
"NotPaid_ExtraTransaction": "Laskua ei ole maksettu kokonaan. Lähetä uusi maksu erääntyvän summan kattamiseksi.",
"Recommended_Fee": "Suositeltu siirtomaksu: {{feeRate}} sat/tavu"
"Recommended_Fee": "Suositeltu siirtomaksu: {{feeRate}} sat/tavu",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Payer avec Changelly",
"Close": "Fermer",
"NotPaid_ExtraTransaction": "La facture n'as pas été réglée dans sa totalité. Veuillez envoyer une autre transaction pour la compléter.",
"Recommended_Fee": "Frais recommandés: {{feeRate}} sat/byte"
"Recommended_Fee": "Frais recommandés: {{feeRate}} sat/byte",
"View receipt": "Voir le reçu"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "תשלום עם Changelly",
"Close": "סגירה",
"NotPaid_ExtraTransaction": "החשבונית לא שולמה במלואה. נא לשלוח תשלום נוסף לכיסוי סכום החוב.",
"Recommended_Fee": "עמלה מומלצת: {{feeRate}} סאט/בית"
"Recommended_Fee": "עמלה מומלצת: {{feeRate}} סאט/בית",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Changelly द्वारा भुगतान करें",
"Close": "बंद करे",
"NotPaid_ExtraTransaction": "चालान का पूरा भुगतान नहीं किया गया है। कृपया चालान राशि पूरी करें।",
"Recommended_Fee": "अनुशंसित शुल्क: {{शुल्क दर}} सैट/बाइट "
"Recommended_Fee": "अनुशंसित शुल्क: {{शुल्क दर}} सैट/बाइट ",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Pay with Changelly",
"Close": "Close",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Fizess Changelly-vel",
"Close": "Bezár",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Վճարել Changelly-ով",
"Close": "Փակել",
"NotPaid_ExtraTransaction": "Հաշիվը ամբողջությամբ չի վճարվել: Խնդրում ենք ուղարկել մեկ այլ գործարք՝ մնացած գումարը վճարելու համար:",
"Recommended_Fee": "Առաջարկվող վճար: {{feeRate}} sat/byte"
"Recommended_Fee": "Առաջարկվող վճար: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Bayar dengan Changelly",
"Close": "Tutup",
"NotPaid_ExtraTransaction": "Tagihan ini belum dibayarkan sepenuhnya. Dimohon untuk membayarkan sisa pembayaran anda.",
"Recommended_Fee": "Biaya yang disarankan: {{feeRate}} sat/byte"
"Recommended_Fee": "Biaya yang disarankan: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Borga með Changelly",
"Close": "Loka",
"NotPaid_ExtraTransaction": "Reikningurinn hefur ekki verið greiddur að fullu. Vinsamlegast greiddu restina.",
"Recommended_Fee": "Ráðlögð þóknun: {{feeRate}} sat/bæti"
"Recommended_Fee": "Ráðlögð þóknun: {{feeRate}} sat/bæti",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Paga con Changelly",
"Close": "Chiudi",
"NotPaid_ExtraTransaction": "La fattura non è stata pagata per intero. Per favore effettua una nuova transazione per coprire l'importo dovuto.",
"Recommended_Fee": "Fee raccomandate: {{feeRate}} sat/byte"
"Recommended_Fee": "Fee raccomandate: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Changellyでのお支払い",
"Close": "閉じる",
"NotPaid_ExtraTransaction": "請求金額の全額が支払われていません。未払い分の別のトランザクションをお送りください。",
"Recommended_Fee": "推奨手数料: {{feeRate}} sat/byte"
"Recommended_Fee": "推奨手数料: {{feeRate}} sat/byte",
"View receipt": "領収書へ"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Pay with Changelly",
"Close": "Close",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Changelly를 이용해 지불하기",
"Close": "닫기",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "추천하는 수수료: {{feeRate}} sat/byte"
"Recommended_Fee": "추천하는 수수료: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Maksāt ar Changelly",
"Close": "Aizvērt",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Betalen met Changelly",
"Close": "Sluiten",
"NotPaid_ExtraTransaction": "Het factuur is niet volledig betaald. Stuur nog een transactie om het resterende bedrag te betalen.",
"Recommended_Fee": "Aanbevolen vergoeding: {{feeRate}} sat/byte"
"Recommended_Fee": "Aanbevolen vergoeding: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Betal med Changelly",
"Close": "Lukk",
"NotPaid_ExtraTransaction": "Denne fakturaen har ikke blitt betalt fullt ut. Send en ny transaksjon med resten av beløpet.",
"Recommended_Fee": "Anbefalt avgift: {{feeRate}} sat/byte"
"Recommended_Fee": "Anbefalt avgift: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Pay with Changelly",
"Close": "Close",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Zapłać z Changelly",
"Close": "Zamknij",
"NotPaid_ExtraTransaction": "Faktura nie została w pełni opłacona. Proszę wyślij dodatkową transakcje z resztą brakujących środków.",
"Recommended_Fee": "Zalecana opłata transakcyjna: {{feeRate}} sat/byte"
"Recommended_Fee": "Zalecana opłata transakcyjna: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Pagar com Changelly",
"Close": "Fechar",
"NotPaid_ExtraTransaction": "A fatura não foi paga integralmente. Envie outra transação para cobrir o valor devido.",
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte"
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Pagar com Changelly",
"Close": "Fechar",
"NotPaid_ExtraTransaction": "A fatura não foi paga na totalidade. Por favor, faça outra transação para cobrir o valor em falta.",
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte"
"Recommended_Fee": "Taxa recomendada: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Platiți cu Changelly",
"Close": "Închideți",
"NotPaid_ExtraTransaction": "Factura nu a fost achitată integral. Vă rugăm să trimiteți o altă plată pentru a acoperi suma datorată.",
"Recommended_Fee": "Taxa recomandată: {{feeRate}} sat/byte"
"Recommended_Fee": "Taxa recomandată: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Оплатить с помощью Changelly",
"Close": "Закрыть",
"NotPaid_ExtraTransaction": "Счет не был оплачен полностью. Пожалуйста, отправьте еще одну транзакцию для покрытия суммы задолженности.",
"Recommended_Fee": "Рекомендуемая комиссия: {{feeRate}} сат/байт"
"Recommended_Fee": "Рекомендуемая комиссия: {{feeRate}} сат/байт",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Zaplatiť cez Changelly",
"Close": "Zatvoriť",
"NotPaid_ExtraTransaction": "Faktúra nebola uhradená v plnej výške. Prosím pošlite inú transakciu na pokrytie dlžnej sumy.",
"Recommended_Fee": "Odporúčaný poplatok: {{feeRate}} sat/bajt"
"Recommended_Fee": "Odporúčaný poplatok: {{feeRate}} sat/bajt",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Plačilo z Changelly",
"Close": "Zapri",
"NotPaid_ExtraTransaction": "Račun ni bil plačan v celoti. Potrebna je nova transakcija za doplačilo.",
"Recommended_Fee": "Priporočena provizija: {{feeRate}} sat/byte"
"Recommended_Fee": "Priporočena provizija: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Plati putem Changelly-ja",
"Close": "Zatvori",
"NotPaid_ExtraTransaction": "Račun nije plaćen u potpunosti. Pošalji još jednu transakciju da izmiriš Dug.",
"Recommended_Fee": "Preporučena provizija: {{feeRate}} sat/byte"
"Recommended_Fee": "Preporučena provizija: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Betala med Changelly",
"Close": "Stäng",
"NotPaid_ExtraTransaction": "Fakturabetalningen är ofullständig. Vänligen betala kvarstående belopp.",
"Recommended_Fee": "Rekommenderad avgift: {{feeRate}} sat/byte"
"Recommended_Fee": "Rekommenderad avgift: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Changelly ile Öde",
"Close": "Kapat",
"NotPaid_ExtraTransaction": "Fatura tam olarak ödenmedi. Lütfen ödenmesi gereken tutarı karşılamak için başka bir işlem gönderin.",
"Recommended_Fee": "Önerilen ücret: {{feeRate}} sat/bayt"
"Recommended_Fee": "Önerilen ücret: {{feeRate}} sat/bayt",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Оплатити за допомогою Changelly",
"Close": "Закрити",
"NotPaid_ExtraTransaction": "Рахунок не був оплачений повністю. Будь ласка, надішліть ще одну транзакцію для покриття суми заборгованості.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Pay with Changelly",
"Close": "Close",
"NotPaid_ExtraTransaction": "The invoice hasn't been paid in full. Please send another transaction to cover amount Due.",
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte"
"Recommended_Fee": "Recommended fee: {{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "通过Changelly支付",
"Close": "关闭",
"NotPaid_ExtraTransaction": "发票还没有付清。请订单到期之前发送另一笔交易完成支付。",
"Recommended_Fee": "推荐费率::{{feeRate}} sat/byte"
"Recommended_Fee": "推荐费率::{{feeRate}} sat/byte",
"View receipt": "View receipt"
}

View file

@ -48,5 +48,6 @@
"Pay with Changelly": "Khokha nge-Changelly",
"Close": "Vala",
"NotPaid_ExtraTransaction": "I-invoice ayikakhokhelwa ngokugcwele. Sicela uthumele enye imali oyokhokha ukuze ukhokhe inani elifanele.",
"Recommended_Fee": "Imali enconyiwe: {{feeRate}} sat / byte"
"Recommended_Fee": "Imali enconyiwe: {{feeRate}} sat / byte",
"View receipt": "View receipt"
}

View file

@ -714,6 +714,11 @@
"nullable": true,
"$ref": "#/components/schemas/CheckoutOptions",
"description": "Additional settings to customize the checkout flow"
},
"receipt": {
"nullable": true,
"$ref": "#/components/schemas/ReceiptOptions",
"description": "Additional settings to customize the public receipt"
}
}
},
@ -1061,6 +1066,29 @@
}
}
},
"ReceiptOptions": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"nullable": true,
"description": "A public page will be accessible once the invoice is settled. If null or unspecified, it will fallback to the store's settings. (The default store settings is true)"
},
"showQR": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Show the QR code of the receipt in the public receipt page. If null or unspecified, it will fallback to the store's settings. (The default store setting is true)"
},
"showPayments": {
"type": "boolean",
"nullable": true,
"default": null,
"description": "Show the payment list in the public receipt page. If null or unspecified, it will fallback to the store's settings. (The default store setting is true)"
}
}
},
"SpeedPolicy": {
"type": "string",
"description": "`\"HighSpeed\"`: 0 confirmations (1 confirmation if RBF enabled in transaction) \n`\"MediumSpeed\"`: 1 confirmation \n`\"LowMediumSpeed\"`: 2 confirmations \n`\"LowSpeed\"`: 6 confirmations\n",

View file

@ -329,6 +329,11 @@
"default": false,
"description": "If true, the checkout page will ask to enter an email address before accessing payment information."
},
"receipt": {
"nullable": true,
"$ref": "#/components/schemas/ReceiptOptions",
"description": "Additional settings to customize the public receipt"
},
"lightningAmountInSatoshi": {
"type": "boolean",
"default": false,