Skip to content

Commit

Permalink
Public Invoice receipt (btcpayserver#3612)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
3 people authored Jul 6, 2022
1 parent 2a190d5 commit 3576ebd
Show file tree
Hide file tree
Showing 71 changed files with 785 additions and 199 deletions.
43 changes: 43 additions & 0 deletions BTCPayServer.Client/Models/InvoiceData.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using BTCPayServer.Client.JsonConverters;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
Expand All @@ -19,6 +20,48 @@ public class InvoiceDataBase
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
{

Expand Down
2 changes: 2 additions & 0 deletions BTCPayServer.Client/Models/StoreBaseData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public abstract class StoreBaseData
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;

public bool PayJoinEnabled { get; set; }

public InvoiceData.ReceiptOptions Receipt { get; set; }


[JsonExtensionData]
Expand Down
2 changes: 1 addition & 1 deletion BTCPayServer.Tests/BTCPayServer.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
33 changes: 33 additions & 0 deletions BTCPayServer.Tests/FastTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,39 @@ List<DockerImage> GetImages(string content)
}
}

[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()
{
Expand Down
12 changes: 11 additions & 1 deletion BTCPayServer.Tests/SeleniumTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,19 @@ public async Task StartAsync()
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>
Expand Down
56 changes: 56 additions & 0 deletions BTCPayServer.Tests/SeleniumTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
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;
Expand Down Expand Up @@ -437,6 +438,61 @@ public async Task CanCreateInvoiceInUI()
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()
Expand Down
2 changes: 1 addition & 1 deletion BTCPayServer/BTCPayServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,8 @@ private InvoiceData ToModel(InvoiceEntity entity)
RedirectAutomatically = entity.RedirectAutomatically,
RequiresRefundEmail = entity.RequiresRefundEmail,
RedirectURL = entity.RedirectURLTemplate
}
},
Receipt = entity.ReceiptOptions
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,11 @@ public class GreenfieldStoresController : ControllerBase
{
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")]
Expand Down Expand Up @@ -129,6 +127,7 @@ private Client.Models.StoreData FromModel(Data.StoreData data)
//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,
Expand Down Expand Up @@ -166,6 +165,7 @@ private static void ToModel(StoreBaseData restModel, Data.StoreData model, Payme
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;
Expand Down
Loading

0 comments on commit 3576ebd

Please sign in to comment.