Compare commits
49 Commits
v1.10.0-rc
...
v1.10.3
Author | SHA1 | Date | |
---|---|---|---|
983b8c1f54 | |||
d666d8ea1a | |||
3ed81c3a78 | |||
4afec2e2b6 | |||
db83d238d5 | |||
fdcf7b3b7a | |||
53aafcf86b | |||
aec84f6d67 | |||
01e9f82d24 | |||
2eff45e65c | |||
13203c3e2b | |||
82c5e0e43d | |||
a1575f404b | |||
e1509506dc | |||
0c1d0d7b05 | |||
ad70856af0 | |||
8615f120ce | |||
0d0477d661 | |||
b31dc30878 | |||
6e392f4cfb | |||
cc3bdc331e | |||
76faf77a1c | |||
d8c0e5bf3a | |||
28c4c320cc | |||
e81403ec3f | |||
f11424f73a | |||
fa8b977016 | |||
d181846339 | |||
1956919886 | |||
0f66498965 | |||
918cd152b1 | |||
d3222df396 | |||
a84ffd8c7e | |||
6d0f9120b8 | |||
aafb4a7f2a | |||
ae432ff237 | |||
cdc318c71a | |||
94d1cec8a9 | |||
c0bc19ea59 | |||
6f07714cd9 | |||
a9d2cac23c | |||
693b46126b | |||
bbff9710bf | |||
358e122775 | |||
3818468932 | |||
3d2554fbe1 | |||
4309603317 | |||
f733c9ea77 | |||
775ee01171 |
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Rating
BTCPayServer.Tests
AltcoinTests
Extensions.csGreenfieldAPITests.csPOSTests.csSeleniumTests.csThirdPartyTests.csUnitTest1.csdocker-compose.altcoins.ymldocker-compose.ymlBTCPayServer
BTCPayServer.csprojExtensions.cs
Components
AppTopItems
StoreRecentInvoices
StoreRecentTransactions
Controllers
GreenField
GreenfieldInvoiceController.csGreenfieldPaymentRequestsController.csGreenfieldPullPaymentController.csGreenfieldStoreOnChainWalletsController.csGreenfieldStoresController.cs
UIInvoiceController.UI.csUIInvoiceController.csUILNURLController.csUIManageController.APIKeys.csUIPullPaymentController.csUIStoresController.Onchain.csUIStoresController.Roles.csUIWalletsController.csHostedServices
Hosting
Models
Payments/Lightning
Plugins
Security/GreenField
Services
Views
Shared
CreateOrEditRole.cshtml
Crowdfund/Public
LayoutHeadTheme.cshtmlNFC
PointOfSale/Public
PosData.cshtmlTemplateEditor.cshtmlUIForms
UIInvoice
UIManage
UIPaymentRequest
UIPullPayment
UIServer
UIStores
UIWallets
wwwroot
cart/js
light-pos
locales
main
swagger/v1
vendor/bootstrap-vue
Build
Changelog.md@ -12,6 +12,8 @@
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<Configurations>Debug;Release;Altcoins-Debug;Altcoins-Release</Configurations>
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.2</Version>
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.3" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.2.5" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
|
@ -15,7 +15,7 @@ namespace BTCPayServer.Rating
|
||||
while (true)
|
||||
{
|
||||
var rounded = decimal.Round(value, divisibility, MidpointRounding.AwayFromZero);
|
||||
if ((Math.Abs(rounded - value) / value) < 0.001m)
|
||||
if ((Math.Abs(rounded - value) / value) < 0.01m)
|
||||
{
|
||||
value = rounded;
|
||||
break;
|
||||
|
@ -669,7 +669,7 @@ donation:
|
||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||
Assert.Equal("15,18,20", string.Join(',', vmview.CustomTipPercentages));
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
|
||||
|
||||
//
|
||||
var invoices = await user.BitPay.GetInvoicesAsync();
|
||||
@ -678,7 +678,7 @@ donation:
|
||||
Assert.Equal("CAD", orangeInvoice.Currency);
|
||||
Assert.Equal("orange", orangeInvoice.ItemDesc);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||
@ -687,7 +687,7 @@ donation:
|
||||
|
||||
// testing custom amount
|
||||
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, null, null, null, null, "donation").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result);
|
||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
|
||||
@ -760,20 +760,20 @@ noninventoryitem:
|
||||
await tester.WaitForEvent<AppInventoryUpdaterHostedService.UpdateAppInventory>(() =>
|
||||
{
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
//we already bought all available stock so this should fail
|
||||
await Task.Delay(100);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "inventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "inventoryitem").Result);
|
||||
|
||||
//inventoryitem has unlimited items available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "noninventoryitem").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "noninventoryitem").Result);
|
||||
|
||||
//verify invoices where created
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
@ -809,9 +809,9 @@ normal:
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "btconly").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "btconly").Result);
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, null, null, null, null, "normal").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 1, choiceKey: "normal").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var normalInvoice = invoices.Single(invoice => invoice.ItemCode == "normal");
|
||||
var btcOnlyInvoice = invoices.Single(invoice => invoice.ItemCode == "btconly");
|
||||
@ -865,7 +865,7 @@ g:
|
||||
Assert.Contains(items, item => item.Id == "g" && item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, null, null, null, null, null, "g").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Static, choiceKey: "g").Result);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
var topupInvoice = invoices.Single(invoice => invoice.ItemCode == "g");
|
||||
Assert.Equal(0, topupInvoice.Price);
|
||||
|
@ -122,6 +122,13 @@ retry:
|
||||
driver.ExecuteJavaScript($"document.getElementById('{element}').{funcName}()");
|
||||
}
|
||||
|
||||
public static void WaitWalletTransactionsLoaded(this IWebDriver driver)
|
||||
{
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
wait.UntilJsIsReady();
|
||||
wait.Until(d => d.WaitForElement(By.CssSelector("#WalletTransactions[data-loaded='true']")));
|
||||
}
|
||||
|
||||
public static IWebElement WaitForElement(this IWebDriver driver, By selector)
|
||||
{
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
|
@ -1074,6 +1074,22 @@ namespace BTCPayServer.Tests
|
||||
var lnrURLs = await unauthenticated.GetPullPaymentLNURL(test4.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||
Assert.Equal(12.303228134m, test4.Amount);
|
||||
Assert.Equal("BTC", test4.Currency);
|
||||
|
||||
// Test with SATS denomination values
|
||||
var testSats = await client.CreatePullPayment(storeId, new Client.Models.CreatePullPaymentRequest()
|
||||
{
|
||||
Name = "Test SATS",
|
||||
Amount = 21000,
|
||||
Currency = "SATS",
|
||||
PaymentMethods = new[] { "BTC", "BTC-LightningNetwork", "BTC_LightningLike" }
|
||||
});
|
||||
lnrURLs = await unauthenticated.GetPullPaymentLNURL(testSats.Id);
|
||||
Assert.IsType<string>(lnrURLs.LNURLBech32);
|
||||
Assert.IsType<string>(lnrURLs.LNURLUri);
|
||||
Assert.Equal(21000, testSats.Amount);
|
||||
Assert.Equal("SATS", testSats.Currency);
|
||||
|
||||
//permission test around auto approved pps and payouts
|
||||
var nonApproved = await acc.CreateClient(Policies.CanCreateNonApprovedPullPayments);
|
||||
@ -2615,7 +2631,7 @@ namespace BTCPayServer.Tests
|
||||
for (int i = 0; i < invoices.Length; i++)
|
||||
{
|
||||
pm[i] = Assert.Single(await client.GetInvoicePaymentMethods(user.StoreId, (await invoices[i]).Id));
|
||||
Assert.False(pm[i].AdditionalData.HasValues);
|
||||
Assert.True(pm[i].AdditionalData.HasValues);
|
||||
}
|
||||
|
||||
// Pay them all at once
|
||||
|
@ -135,10 +135,10 @@ donation:
|
||||
Assert.Equal("donation", vmview.Items[1].Title);
|
||||
// orange is available
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "orange").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "orange").Result);
|
||||
// apple is not found
|
||||
Assert.IsType<NotFoundResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, null, null, null, null, "apple").Result);
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@ -965,7 +966,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("ToggleRawEditor")).Click();
|
||||
|
||||
var template = s.Driver.FindElement(By.Id("Template")).GetAttribute("value");
|
||||
Assert.Contains("\"buyButtonText\":\"Take my money\"", template);
|
||||
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
@ -1441,7 +1442,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("CancelWizard")).Click();
|
||||
|
||||
// Check the label is applied to the tx
|
||||
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
Assert.Equal("label2", s.Driver.FindElement(By.XPath("//*[@id=\"WalletTransactionsList\"]//*[contains(@class, 'transaction-label')]")).Text);
|
||||
|
||||
//change the wallet and ensure old address is not there and generating a new one does not result in the prev one
|
||||
@ -1492,7 +1493,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Check the tx sent earlier arrived
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
Assert.Contains(tx.ToString(), s.Driver.PageSource);
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
s.Driver.FindElement(By.PartialLinkText(tx.ToString()));
|
||||
|
||||
var walletTransactionUri = new Uri(s.Driver.Url);
|
||||
|
||||
// Send to bob
|
||||
@ -1618,9 +1621,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Transactions list is empty
|
||||
s.Driver.FindElement(By.Id($"StoreNav-Wallet{cryptoCode}")).Click();
|
||||
Assert.Contains("There are no transactions yet.", s.Driver.PageSource);
|
||||
s.Driver.AssertElementNotFound(By.Id("ExportDropdownToggle"));
|
||||
s.Driver.AssertElementNotFound(By.Id("ActionsDropdownToggle"));
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
Assert.Contains("There are no transactions yet", s.Driver.FindElement(By.Id("WalletTransactions")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -1748,7 +1750,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(labels, element => element.Text == "pull-payment");
|
||||
});
|
||||
|
||||
|
||||
s.GoToStore(s.StoreId, StoreNavPages.Payouts);
|
||||
s.Driver.FindElement(By.Id($"{PayoutState.InProgress}-view")).Click();
|
||||
ReadOnlyCollection<IWebElement> txs;
|
||||
@ -1932,8 +1933,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Contains(PayoutState.AwaitingPayment.GetStateString(), s.Driver.PageSource);
|
||||
|
||||
//lnurl-w support check
|
||||
|
||||
// LNURL Withdraw support check with BTC denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
@ -2001,6 +2001,42 @@ namespace BTCPayServer.Tests
|
||||
|
||||
Assert.Contains(PayoutState.AwaitingApproval.GetStateString(), s.Driver.PageSource);
|
||||
});
|
||||
|
||||
// LNURL Withdraw support check with SATS denomination
|
||||
s.GoToStore(s.StoreId, StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP SATS");
|
||||
s.Driver.SetCheckbox(By.Id("AutoApproveClaims"), true);
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("21021");
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("SATS" + Keys.Enter);
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
|
||||
s.Driver.FindElement(By.CssSelector("button[data-bs-dismiss='modal']")).Click();
|
||||
var amount = new LightMoney(21021, LightMoneyUnit.Satoshi);
|
||||
info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(lnurl, s.Server.PayTester.HttpClient));
|
||||
Assert.Equal(amount, info.MaxWithdrawable);
|
||||
Assert.Equal(amount, info.CurrentBalance);
|
||||
info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(info.BalanceCheck, s.Server.PayTester.HttpClient));
|
||||
Assert.Equal(amount, info.MaxWithdrawable);
|
||||
Assert.Equal(amount, info.CurrentBalance);
|
||||
|
||||
bolt2 = (await s.Server.CustomerLightningD.CreateInvoice(
|
||||
amount,
|
||||
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None));
|
||||
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
Assert.Contains(bolt2.BOLT11, s.Driver.PageSource);
|
||||
|
||||
Assert.Contains(PayoutState.Completed.GetStateString(), s.Driver.PageSource);
|
||||
Assert.Equal(LightningInvoiceStatus.Paid, (await s.Server.CustomerLightningD.GetInvoice(bolt2.Id)).Status);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -2040,6 +2076,81 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
public async Task CanUsePOSKeypad()
|
||||
{
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
s.CreateNewStore();
|
||||
s.GoToStore();
|
||||
s.AddLightningNode(LightningConnectionType.CLightning, false);
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(Guid.NewGuid().ToString());
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
TestUtils.Eventually(() => Assert.Contains("App successfully created", s.FindAlertMessage().Text));
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Light']")).Click();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("EUR");
|
||||
s.Driver.FindElement(By.Id("CustomTipPercentages")).Clear();
|
||||
s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.WaitForElement(By.ClassName("keypad"));
|
||||
|
||||
// basic checks
|
||||
Assert.Contains("EUR", s.Driver.FindElement(By.Id("Currency")).Text);
|
||||
Assert.Contains("0,00", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-amount")).Selected);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
|
||||
|
||||
// Amount: 1234,56
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='2']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='3']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='4']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='.']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='5']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='6']")).Click();
|
||||
Assert.Equal("1.234,56", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-discount")).Enabled);
|
||||
Assert.True(s.Driver.FindElement(By.Id("ModeTablist-tip")).Enabled);
|
||||
Assert.Equal("", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Discount: 10%
|
||||
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-discount']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='1']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".keypad [data-key='0']")).Click();
|
||||
Assert.Contains("1.111,10", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Contains("10% discount", s.Driver.FindElement(By.Id("Discount")).Text);
|
||||
Assert.Contains("1.234,56 € - 123,46 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Tip: 10%
|
||||
s.Driver.FindElement(By.CssSelector("label[for='ModeTablist-tip']")).Click();
|
||||
s.Driver.WaitForElement(By.Id("Tip-Custom"));
|
||||
s.Driver.FindElement(By.Id("Tip-10")).Click();
|
||||
Assert.Contains("1.222,21", s.Driver.FindElement(By.Id("Amount")).Text);
|
||||
Assert.Contains("1.234,56 € - 123,46 € (10%) + 111,11 € (10%)", s.Driver.FindElement(By.Id("Calculation")).Text);
|
||||
|
||||
// Pay
|
||||
s.Driver.FindElement(By.Id("pay-button")).Click();
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
|
||||
Assert.Contains("1 222,21 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Lightning", "Lightning")]
|
||||
@ -2269,7 +2380,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var addresses = s.Driver.FindElements(By.ClassName("lightning-address-value"));
|
||||
Assert.Equal(2, addresses.Count);
|
||||
|
||||
var callbacks = new List<Uri>();
|
||||
foreach (IWebElement webElement in addresses)
|
||||
{
|
||||
var value = webElement.GetAttribute("value");
|
||||
@ -2287,6 +2398,7 @@ namespace BTCPayServer.Tests
|
||||
lnaddress2 = m["text/identifier"];
|
||||
Assert.Equal(2, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.Equal(10, request.MaxSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
callbacks.Add(request.Callback);
|
||||
break;
|
||||
|
||||
case { } v when v.StartsWith(lnaddress1):
|
||||
@ -2294,6 +2406,7 @@ namespace BTCPayServer.Tests
|
||||
lnaddress1 = m["text/identifier"];
|
||||
Assert.Equal(1, request.MinSendable.ToDecimal(LightMoneyUnit.Satoshi));
|
||||
Assert.Equal(6.12m, request.MaxSendable.ToDecimal(LightMoneyUnit.BTC));
|
||||
callbacks.Add(request.Callback);
|
||||
break;
|
||||
default:
|
||||
Assert.False(true, "Should have matched");
|
||||
@ -2301,7 +2414,19 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
var repo = s.Server.PayTester.GetService<InvoiceRepository>();
|
||||
|
||||
var invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
|
||||
// Resolving a ln address shouldn't create any btcpay invoice.
|
||||
// This must be done because some NOST clients resolve ln addresses preemptively without user interaction
|
||||
Assert.Empty(invoices);
|
||||
|
||||
// Calling the callbacks should create the invoices
|
||||
foreach (var callback in callbacks)
|
||||
{
|
||||
using var r = await s.Server.PayTester.HttpClient.GetAsync(callback);
|
||||
await r.Content.ReadAsStringAsync();
|
||||
}
|
||||
invoices = await repo.GetInvoices(new InvoiceQuery() { StoreId = new[] { s.StoreId } });
|
||||
Assert.Equal(2, invoices.Length);
|
||||
var emailSuffix = $"@{s.Server.PayTester.HostName}:{s.Server.PayTester.Port}";
|
||||
foreach (var i in invoices)
|
||||
|
@ -306,6 +306,8 @@ retry:
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = value.GetAwaiter().GetResult();
|
||||
if (key.ToString() == "BTG_USD")
|
||||
continue; // shitcoin not supported by bitfinex anymore
|
||||
TestLogs.LogInformation($"Testing {key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
@ -363,6 +365,11 @@ retry:
|
||||
version = Regex.Match(actual, "Sortable ([0-9]+.[0-9]+.[0-9]+) ").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://unpkg.com/sortablejs@{version}/Sortable.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "bootstrap-vue", "bootstrap-vue.min.js").Trim();
|
||||
version = Regex.Match(actual, "BootstrapVue ([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdnjs.cloudflare.com/ajax/libs/bootstrap-vue/{version}/bootstrap-vue.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
string GetFileContent(params string[] path)
|
||||
|
@ -39,6 +39,7 @@ using BTCPayServer.Plugins.PayButton;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Security.Bitpay;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -718,7 +719,7 @@ namespace BTCPayServer.Tests
|
||||
btcDerivationScheme.GetDerivation(new KeyPath("0/90")).ScriptPubKey, Money.Coins(1.0m));
|
||||
tester.ExplorerNode.Generate(1);
|
||||
var transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
Assert.Empty(transactions.Transactions);
|
||||
|
||||
Assert.IsType<RedirectToActionResult>(walletController.WalletRescan(walletId, rescan).Result);
|
||||
@ -747,7 +748,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.NotNull(rescan.TimeOfScan);
|
||||
Assert.Equal(1, rescan.LastSuccess.Found);
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
var tx = Assert.Single(transactions.Transactions);
|
||||
Assert.Equal(tx.Id, txId.ToString());
|
||||
|
||||
@ -762,7 +763,7 @@ namespace BTCPayServer.Tests
|
||||
await walletController.ModifyTransaction(walletId, tx.Id, addcomment: "hello"));
|
||||
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
@ -774,7 +775,7 @@ namespace BTCPayServer.Tests
|
||||
await walletController.ModifyTransaction(walletId, tx.Id, removelabel: "test2"));
|
||||
|
||||
transactions = Assert.IsType<ListTransactionsViewModel>(Assert
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId).Result).Model);
|
||||
.IsType<ViewResult>(walletController.WalletTransactions(walletId, loadTransactions: true).Result).Model);
|
||||
tx = Assert.Single(transactions.Transactions);
|
||||
|
||||
Assert.Equal("hello", tx.Comment);
|
||||
@ -1984,6 +1985,7 @@ namespace BTCPayServer.Tests
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess(true);
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
var btcpayClient = await user.CreateClient();
|
||||
|
||||
DateTimeOffset expiration = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(21);
|
||||
|
||||
@ -2064,6 +2066,20 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var zeroInvoicePM = await greenfield.GetInvoicePaymentMethods(user.StoreId, zeroInvoice.Id);
|
||||
Assert.Empty(zeroInvoicePM);
|
||||
|
||||
var invoice6 = await btcpayClient.CreateInvoice(user.StoreId,
|
||||
new CreateInvoiceRequest()
|
||||
{
|
||||
Amount = GreenfieldConstants.MaxAmount,
|
||||
Currency = "USD"
|
||||
});
|
||||
var repo = tester.PayTester.GetService<InvoiceRepository>();
|
||||
var entity = (await repo.GetInvoice(invoice6.Id));
|
||||
Assert.Equal((decimal)ulong.MaxValue, entity.Price);
|
||||
entity.GetPaymentMethods().First().Calculate();
|
||||
// Shouldn't be possible as we clamp the value, but existing invoice may have that
|
||||
entity.Price = decimal.MaxValue;
|
||||
entity.GetPaymentMethods().First().Calculate();
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
|
@ -73,7 +73,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:24.1-1
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -135,7 +135,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:24.1-1
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -224,7 +224,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.3-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -259,7 +259,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.3-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -70,7 +70,7 @@ services:
|
||||
- "sshd_datadir:/root/.ssh"
|
||||
|
||||
devlnd:
|
||||
image: btcpayserver/bitcoin:24.1-1
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -121,7 +121,7 @@ services:
|
||||
|
||||
bitcoind:
|
||||
restart: unless-stopped
|
||||
image: btcpayserver/bitcoin:24.1-1
|
||||
image: btcpayserver/bitcoin:25.0
|
||||
environment:
|
||||
BITCOIN_NETWORK: regtest
|
||||
BITCOIN_WALLETDIR: "/data/wallets"
|
||||
@ -211,7 +211,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.3-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -248,7 +248,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.2-beta
|
||||
image: btcpayserver/lnd:v0.16.3-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -48,7 +48,7 @@
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.26" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.28" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
|
@ -40,7 +40,7 @@ public class AppTopItems : ViewComponent
|
||||
var app = HttpContext.GetAppData();
|
||||
var entries = await _appService.GetItemStats(app);
|
||||
vm.SalesCount = entries.Select(e => e.SalesCount).ToList();
|
||||
vm.Entries = entries.ToList();
|
||||
vm.Entries = entries.Take(5).ToList();
|
||||
vm.AppType = app.AppType;
|
||||
vm.AppUrl = await appBaseType.ConfigureLink(app);
|
||||
vm.Name = app.Name;
|
||||
|
@ -51,6 +51,10 @@
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
@if (invoice.Details.Archived)
|
||||
{
|
||||
<span class="badge bg-warning">archived</span>
|
||||
}
|
||||
<span class="badge badge-@invoice.Status.Status.ToModernStatus().ToString().ToLower()">
|
||||
@invoice.Status.Status.ToModernStatus().ToString()
|
||||
@if (invoice.Status.ExceptionStatus != InvoiceExceptionStatus.None)
|
||||
@ -58,6 +62,10 @@
|
||||
@($"({invoice.Status.ExceptionStatus.ToString()})")
|
||||
}
|
||||
</span>
|
||||
@foreach (var paymentType in invoice.Details.Payments.Select(payment => payment.GetPaymentMethodId()?.PaymentType).Distinct().Where(type => type != null && !string.IsNullOrEmpty(type.GetBadge())))
|
||||
{
|
||||
<span class="badge">@paymentType.GetBadge()</span>
|
||||
}
|
||||
@if (invoice.HasRefund)
|
||||
{
|
||||
<span class="badge bg-warning">
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
@ -11,5 +12,7 @@ public class StoreRecentInvoiceViewModel
|
||||
public string Currency { get; set; }
|
||||
public InvoiceState Status { get; set; }
|
||||
public DateTimeOffset Date { get; set; }
|
||||
|
||||
public InvoiceDetailsModel Details { get; set; }
|
||||
public bool HasRefund { get; set; }
|
||||
}
|
||||
|
@ -3,11 +3,13 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Components.StoreRecentInvoices;
|
||||
|
||||
@ -53,17 +55,22 @@ public class StoreRecentInvoices : ViewComponent
|
||||
});
|
||||
|
||||
vm.Invoices = (from invoice in invoiceEntities
|
||||
let state = invoice.GetInvoiceState()
|
||||
select new StoreRecentInvoiceViewModel
|
||||
{
|
||||
Date = invoice.InvoiceTime,
|
||||
Status = state,
|
||||
HasRefund = invoice.Refunds.Any(),
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
Amount = invoice.Price,
|
||||
Currency = invoice.Currency
|
||||
}).ToList();
|
||||
let state = invoice.GetInvoiceState()
|
||||
select new StoreRecentInvoiceViewModel
|
||||
{
|
||||
Date = invoice.InvoiceTime,
|
||||
Status = state,
|
||||
HasRefund = invoice.Refunds.Any(),
|
||||
InvoiceId = invoice.Id,
|
||||
OrderId = invoice.Metadata.OrderId ?? string.Empty,
|
||||
Amount = invoice.Price,
|
||||
Currency = invoice.Currency,
|
||||
Details = new InvoiceDetailsModel
|
||||
{
|
||||
Archived = invoice.Archived,
|
||||
Payments = invoice.GetPayments(false)
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ public class StoreRecentTransactions : ViewComponent
|
||||
{
|
||||
var network = derivationSettings.Network;
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0));
|
||||
var allTransactions = await wallet.FetchTransactionHistory(derivationSettings.AccountDerivation, 0, 5, TimeSpan.FromDays(31.0), cancellationToken: this.HttpContext.RequestAborted);
|
||||
var walletTransactionsInfo = await _walletRepository.GetWalletTransactionsInfo(vm.WalletId, allTransactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
|
||||
transactions = allTransactions
|
||||
|
@ -12,6 +12,7 @@ using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -183,6 +184,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), "The amount should be 0 or more.");
|
||||
}
|
||||
if (request.Amount > GreenfieldConstants.MaxAmount)
|
||||
{
|
||||
ModelState.AddModelError(nameof(request.Amount), $"The amount should less than {GreenfieldConstants.MaxAmount}.");
|
||||
}
|
||||
request.Checkout ??= new CreateInvoiceRequest.CheckoutOptions();
|
||||
if (request.Checkout.PaymentMethods?.Any() is true)
|
||||
{
|
||||
|
@ -255,7 +255,9 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Email = blob.Email,
|
||||
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
|
||||
EmbeddedCSS = blob.EmbeddedCSS,
|
||||
CustomCSSLink = blob.CustomCSSLink
|
||||
CustomCSSLink = blob.CustomCSSLink,
|
||||
FormResponse = blob.FormResponse,
|
||||
FormId = blob.FormId
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -255,16 +255,15 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return PullPaymentNotFound();
|
||||
|
||||
var blob = pp.GetBlob();
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id => id.PaymentType == LightningPaymentType.Instance && _networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
if (pms is not null && blob.Currency.Equals(pms.CryptoCode, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (_pullPaymentService.SupportsLNURL(blob))
|
||||
{
|
||||
var lnurlEndpoint = new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new
|
||||
{
|
||||
cryptoCode = _networkProvider.DefaultNetwork.CryptoCode,
|
||||
pullPaymentId = pullPaymentId
|
||||
pullPaymentId
|
||||
}, Request.Scheme, Request.Host.ToString())!);
|
||||
|
||||
return base.Ok(new PullPaymentLNURL()
|
||||
return base.Ok(new PullPaymentLNURL
|
||||
{
|
||||
LNURLBech32 = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", true).ToString(),
|
||||
LNURLUri = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", false).ToString()
|
||||
|
@ -182,7 +182,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[FromQuery] TransactionStatus[]? statusFilter = null,
|
||||
[FromQuery] string? labelFilter = null,
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int limit = int.MaxValue
|
||||
[FromQuery] int limit = int.MaxValue,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (IsInvalidWalletRequest(cryptoCode, out var network,
|
||||
@ -197,7 +198,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
if (statusFilter?.Any() is true || !string.IsNullOrWhiteSpace(labelFilter))
|
||||
preFiltering = false;
|
||||
var txs = await wallet.FetchTransactionHistory(derivationScheme.AccountDerivation, preFiltering ? skip : 0,
|
||||
preFiltering ? limit : int.MaxValue);
|
||||
preFiltering ? limit : int.MaxValue, cancellationToken: cancellationToken);
|
||||
if (!preFiltering)
|
||||
{
|
||||
var filteredList = new List<TransactionHistoryLine>(txs.Count);
|
||||
|
@ -129,6 +129,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
|
||||
//we do not include PaymentMethodCriteria because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
NetworkFeeMode = storeBlob.NetworkFeeMode,
|
||||
DefaultCurrency = storeBlob.DefaultCurrency,
|
||||
RequiresRefundEmail = storeBlob.RequiresRefundEmail,
|
||||
CheckoutType = storeBlob.CheckoutType,
|
||||
Receipt = InvoiceDataBase.ReceiptOptions.Merge(storeBlob.ReceiptOptions, null),
|
||||
|
@ -172,7 +172,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("i/{invoiceId}/receipt")]
|
||||
public async Task<IActionResult> InvoiceReceipt(string invoiceId)
|
||||
public async Task<IActionResult> InvoiceReceipt(string invoiceId, [FromQuery] bool print = false)
|
||||
{
|
||||
var i = await _InvoiceRepository.GetInvoice(invoiceId);
|
||||
if (i is null)
|
||||
@ -255,7 +255,7 @@ namespace BTCPayServer.Controllers
|
||||
vm.Payments = receipt.ShowPayments is false ? null : payments;
|
||||
vm.AdditionalData = PosDataParser.ParsePosData(receiptData);
|
||||
|
||||
return View(vm);
|
||||
return View(print ? "InvoiceReceiptPrint" : "InvoiceReceipt", vm);
|
||||
}
|
||||
|
||||
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
|
||||
@ -456,7 +456,7 @@ namespace BTCPayServer.Controllers
|
||||
model.Title = "How much to refund?";
|
||||
model.RefundStep = RefundSteps.SelectRate;
|
||||
|
||||
if (isPaidOver)
|
||||
if (!isPaidOver)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.SelectedRefundOption), "Invoice is not overpaid");
|
||||
}
|
||||
@ -466,7 +466,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return this.CreateValidationError(ModelState);
|
||||
return View("_RefundModal", model);
|
||||
}
|
||||
|
||||
createPullPayment.Currency = paymentMethodId.CryptoCode;
|
||||
|
@ -17,6 +17,7 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
using BTCPayServer.Security.Greenfield;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@ -127,6 +128,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
throw new BitpayHttpException(400, "The expirationTime is set too soon");
|
||||
}
|
||||
if (entity.Price < 0.0m)
|
||||
{
|
||||
throw new BitpayHttpException(400, "The price should be 0 or more.");
|
||||
}
|
||||
if (entity.Price > GreenfieldConstants.MaxAmount)
|
||||
{
|
||||
throw new BitpayHttpException(400, $"The price should less than {GreenfieldConstants.MaxAmount}.");
|
||||
}
|
||||
entity.Metadata.OrderId = invoice.OrderId;
|
||||
entity.Metadata.PosDataLegacy = invoice.PosData;
|
||||
entity.ServerUrl = serverUrl;
|
||||
@ -278,6 +287,7 @@ namespace BTCPayServer.Controllers
|
||||
if (string.IsNullOrEmpty(entity.Currency))
|
||||
entity.Currency = storeBlob.DefaultCurrency;
|
||||
entity.Currency = entity.Currency.Trim().ToUpperInvariant();
|
||||
entity.Price = Math.Min(GreenfieldConstants.MaxAmount, entity.Price);
|
||||
entity.Price = Math.Max(0.0m, entity.Price);
|
||||
var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(entity.Currency, false);
|
||||
if (currencyInfo != null)
|
||||
|
@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@ -109,24 +110,24 @@ namespace BTCPayServer
|
||||
}
|
||||
|
||||
var blob = pp.GetBlob();
|
||||
if (!blob.Currency.Equals(cryptoCode, StringComparison.InvariantCultureIgnoreCase))
|
||||
if (!_pullPaymentHostedService.SupportsLNURL(blob))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var unit = blob.Currency == "SATS" ? LightMoneyUnit.Satoshi : LightMoneyUnit.BTC;
|
||||
var progress = _pullPaymentHostedService.CalculatePullPaymentProgress(pp, DateTimeOffset.UtcNow);
|
||||
|
||||
var remaining = progress.Limit - progress.Completed - progress.Awaiting;
|
||||
var request = new LNURLWithdrawRequest
|
||||
{
|
||||
MaxWithdrawable = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC),
|
||||
MaxWithdrawable = LightMoney.FromUnit(remaining, unit),
|
||||
K1 = pullPaymentId,
|
||||
BalanceCheck = new Uri(Request.GetCurrentUrl()),
|
||||
CurrentBalance = LightMoney.FromUnit(remaining, LightMoneyUnit.BTC),
|
||||
CurrentBalance = LightMoney.FromUnit(remaining, unit),
|
||||
MinWithdrawable =
|
||||
LightMoney.FromUnit(
|
||||
Math.Min(await _lightningLikePayoutHandler.GetMinimumPayoutAmount(pmi, null), remaining),
|
||||
LightMoneyUnit.BTC),
|
||||
unit),
|
||||
Tag = "withdrawRequest",
|
||||
Callback = new Uri(Request.GetCurrentUrl()),
|
||||
// It's not `pp.GetBlob().Description` because this would be HTML
|
||||
@ -154,13 +155,13 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest()
|
||||
var claimResponse = await _pullPaymentHostedService.Claim(new ClaimRequest
|
||||
{
|
||||
Destination = new BoltInvoiceClaimDestination(pr, result),
|
||||
PaymentMethodId = pmi,
|
||||
PullPaymentId = pullPaymentId,
|
||||
StoreId = pp.StoreId,
|
||||
Value = result.MinimumAmount.ToDecimal(LightMoneyUnit.BTC)
|
||||
Value = result.MinimumAmount.ToDecimal(unit)
|
||||
});
|
||||
|
||||
if (claimResponse.Result != ClaimRequest.ClaimResult.Ok)
|
||||
@ -373,13 +374,52 @@ namespace BTCPayServer
|
||||
return NotFound("Unknown username");
|
||||
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
var cryptoCode = "BTC";
|
||||
if (store is null)
|
||||
return NotFound("Unknown username");
|
||||
if (GetLNUrlPaymentMethodId(cryptoCode, store, out var lnUrlMethod) is null)
|
||||
return NotFound("LNUrl not available for store");
|
||||
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
|
||||
return await GetLNURLRequest(
|
||||
"BTC",
|
||||
var lnurlRequest = new LNURLPayRequest()
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
|
||||
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
|
||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0
|
||||
};
|
||||
NormalizeSendable(lnurlRequest);
|
||||
|
||||
var lnUrlMetadata = new Dictionary<string, string>()
|
||||
{
|
||||
["text/identifier"] = $"{username}@{Request.Host}"
|
||||
};
|
||||
SetLNUrlDescriptionMetadata(lnUrlMetadata, store, store.GetStoreBlob(), null);
|
||||
lnurlRequest.Metadata =
|
||||
JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||
|
||||
lnurlRequest.Callback = new Uri(_linkGenerator.GetUriByAction(
|
||||
action: nameof(GetLNURLForLightningAddress),
|
||||
controller: "UILNURL",
|
||||
values: new { cryptoCode, username }, Request.Scheme, Request.Host, Request.PathBase));
|
||||
|
||||
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
|
||||
return Ok(lnurlRequest);
|
||||
}
|
||||
|
||||
[HttpGet("pay/lnaddress/{username}")]
|
||||
[EnableCors(CorsPolicies.All)]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> GetLNURLForLightningAddress(string cryptoCode, string username, [FromQuery] long? amount = null, string comment = null)
|
||||
{
|
||||
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
|
||||
if (lightningAddressSettings is null || username is null)
|
||||
return NotFound("Unknown username");
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
var result = await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
store.GetStoreBlob(),
|
||||
new CreateInvoiceRequest()
|
||||
@ -396,6 +436,10 @@ namespace BTCPayServer
|
||||
{
|
||||
{ "text/identifier", $"{username}@{Request.Host}" }
|
||||
});
|
||||
if (result is not OkObjectResult ok || ok.Value is not LNURLPayRequest payRequest)
|
||||
return result;
|
||||
var invoiceId = payRequest.Callback.AbsoluteUri.Split('/').Last();
|
||||
return await GetLNURLForInvoice(invoiceId, cryptoCode, amount, comment);
|
||||
}
|
||||
|
||||
|
||||
@ -482,11 +526,7 @@ namespace BTCPayServer
|
||||
|
||||
if (!lnUrlMetadata.ContainsKey("text/plain"))
|
||||
{
|
||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", i.Metadata.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", i.Metadata.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
lnUrlMetadata.Add("text/plain", invoiceDescription);
|
||||
SetLNUrlDescriptionMetadata(lnUrlMetadata, store, blob, i.Metadata);
|
||||
}
|
||||
|
||||
lnurlRequest.Tag = "payRequest";
|
||||
@ -503,12 +543,7 @@ namespace BTCPayServer
|
||||
lnurlRequest.MaxSendable = lnurlRequest.MinSendable;
|
||||
}
|
||||
|
||||
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat.
|
||||
if (lnurlRequest.MinSendable is null || lnurlRequest.MinSendable < LightMoney.Satoshis(1.0m))
|
||||
lnurlRequest.MinSendable = LightMoney.Satoshis(1.0m);
|
||||
|
||||
if (lnurlRequest.MaxSendable is null)
|
||||
lnurlRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
|
||||
NormalizeSendable(lnurlRequest);
|
||||
|
||||
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
|
||||
if (paymentMethodDetails.PayRequest is null)
|
||||
@ -524,6 +559,25 @@ namespace BTCPayServer
|
||||
return lnurlRequest;
|
||||
}
|
||||
|
||||
private void SetLNUrlDescriptionMetadata(Dictionary<string, string> lnUrlMetadata, Data.StoreData store, StoreBlob blob, InvoiceMetadata invoiceMetadata)
|
||||
{
|
||||
var invoiceDescription = blob.LightningDescriptionTemplate
|
||||
.Replace("{StoreName}", store.StoreName ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{ItemDescription}", invoiceMetadata?.ItemDesc ?? "", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{OrderId}", invoiceMetadata?.OrderId ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
lnUrlMetadata.Add("text/plain", invoiceDescription);
|
||||
}
|
||||
|
||||
private static void NormalizeSendable(LNURLPayRequest lnurlRequest)
|
||||
{
|
||||
// We don't think BTCPay handle well 0 sats payments, just in case make it minimum one sat.
|
||||
if (lnurlRequest.MinSendable is null || lnurlRequest.MinSendable < LightMoney.Satoshis(1.0m))
|
||||
lnurlRequest.MinSendable = LightMoney.Satoshis(1.0m);
|
||||
|
||||
if (lnurlRequest.MaxSendable is null)
|
||||
lnurlRequest.MaxSendable = LightMoney.FromUnit(6.12m, LightMoneyUnit.BTC);
|
||||
}
|
||||
|
||||
PaymentMethodId GetLNUrlPaymentMethodId(string cryptoCode, Data.StoreData store, out LNURLPaySupportedPaymentMethod lnUrlSettings)
|
||||
{
|
||||
lnUrlSettings = null;
|
||||
|
@ -506,57 +506,57 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public static readonly Dictionary<string, (string Title, string Description)> PermissionDescriptions = new Dictionary<string, (string Title, string Description)>()
|
||||
{
|
||||
{Policies.Unrestricted, ("Unrestricted access", "The app will have unrestricted access to your account.")},
|
||||
{Policies.CanViewUsers, ("View users", "The app will be able to see all users on this server.")},
|
||||
{Policies.CanCreateUser, ("Create new users", "The app will be able to create new users on this server.")},
|
||||
{Policies.CanManageUsers, ("Manage users", "The app will be able to create/delete API keys for users.")},
|
||||
{Policies.CanDeleteUser, ("Delete user", "The app will be able to delete the user to whom it is assigned. Admin users can delete any user without this permission.")},
|
||||
{Policies.CanModifyStoreSettings, ("Modify your stores", "The app will be able to manage invoices on all your stores and modify their settings.")},
|
||||
{$"{Policies.CanModifyStoreSettings}:", ("Manage selected stores", "The app will be able to manage invoices on the selected stores and modify their settings.")},
|
||||
{Policies.CanViewCustodianAccounts, ("View exchange accounts linked to your stores", "The app will be able to see exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanViewCustodianAccounts}:", ("View exchange accounts linked to selected stores", "The app will be able to see exchange accounts linked to the selected stores.")},
|
||||
{Policies.CanManageCustodianAccounts, ("Manage exchange accounts linked to your stores", "The app will be able to modify exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanManageCustodianAccounts}:", ("Manage exchange accounts linked to selected stores", "The app will be able to modify exchange accounts linked to selected stores.")},
|
||||
{Policies.CanDepositToCustodianAccounts, ("Deposit funds to exchange accounts linked to your stores", "The app will be able to deposit funds to your exchange accounts.")},
|
||||
{$"{Policies.CanDepositToCustodianAccounts}:", ("Deposit funds to exchange accounts linked to selected stores", "The app will be able to deposit funds to selected store's exchange accounts.")},
|
||||
{Policies.CanWithdrawFromCustodianAccounts, ("Withdraw funds from exchange accounts to your store", "The app will be able to withdraw funds from your exchange accounts to your store.")},
|
||||
{$"{Policies.CanWithdrawFromCustodianAccounts}:", ("Withdraw funds from selected store's exchange accounts", "The app will be able to withdraw funds from your selected store's exchange accounts.")},
|
||||
{Policies.CanTradeCustodianAccount, ("Trade funds on your store's exchange accounts", "The app will be able to trade funds on your store's exchange accounts.")},
|
||||
{$"{Policies.CanTradeCustodianAccount}:", ("Trade funds on selected store's exchange accounts", "The app will be able to trade funds on selected store's exchange accounts.")},
|
||||
{Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "The app will modify the webhooks of all your stores.")},
|
||||
{$"{Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "The app will modify the webhooks of the selected stores.")},
|
||||
{Policies.CanViewStoreSettings, ("View your stores", "The app will be able to view stores settings.")},
|
||||
{$"{Policies.CanViewStoreSettings}:", ("View your stores", "The app will be able to view the selected stores' settings.")},
|
||||
{Policies.CanModifyServerSettings, ("Manage your server", "The app will have total control on the server settings of your server.")},
|
||||
{Policies.CanViewProfile, ("View your profile", "The app will be able to view your user profile.")},
|
||||
{Policies.CanModifyProfile, ("Manage your profile", "The app will be able to view and modify your user profile.")},
|
||||
{Policies.CanManageNotificationsForUser, ("Manage your notifications", "The app will be able to view and modify your user notifications.")},
|
||||
{Policies.CanViewNotificationsForUser, ("View your notifications", "The app will be able to view your user notifications.")},
|
||||
{Policies.CanCreateInvoice, ("Create an invoice", "The app will be able to create new invoices.")},
|
||||
{$"{Policies.CanCreateInvoice}:", ("Create an invoice", "The app will be able to create new invoices on the selected stores.")},
|
||||
{Policies.CanViewInvoices, ("View invoices", "The app will be able to view invoices.")},
|
||||
{Policies.CanModifyInvoices, ("Modify invoices", "The app will be able to modify and view invoices.")},
|
||||
{$"{Policies.CanViewInvoices}:", ("View invoices", "The app will be able to view invoices on the selected stores.")},
|
||||
{$"{Policies.CanModifyInvoices}:", ("Modify invoices", "The app will be able to modify and view invoices on the selected stores.")},
|
||||
{Policies.CanModifyPaymentRequests, ("Modify your payment requests", "The app will be able to view, modify, delete and create new payment requests on all your stores.")},
|
||||
{$"{Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "The app will be able to view, modify, delete and create new payment requests on the selected stores.")},
|
||||
{Policies.CanViewPaymentRequests, ("View your payment requests", "The app will be able to view payment requests.")},
|
||||
{$"{Policies.CanViewPaymentRequests}:", ("View your payment requests", "The app will be able to view the selected stores' payment requests.")},
|
||||
{Policies.CanManagePullPayments, ("Manage your pull payments", "The app will be able to view, modify, delete and create pull payments on all your stores.")},
|
||||
{$"{Policies.CanManagePullPayments}:", ("Manage selected stores' pull payments", "The app will be able to view, modify, delete and create new pull payments on the selected stores.")},
|
||||
{Policies.CanCreatePullPayments, ("Create pull payments", "The app will be able to create pull payments on all your stores.")},
|
||||
{$"{Policies.CanCreatePullPayments}:", ("Create pull payments in selected stores", "The app will be able to create new pull payments on the selected stores.")},
|
||||
{Policies.CanCreateNonApprovedPullPayments, ("Create non-approved pull payments", "The app will be able to create pull payments without automatic approval on all your stores.")},
|
||||
{$"{Policies.CanCreateNonApprovedPullPayments}:", ("Create non-approved pull payments in selected stores", "The app will be able to view, modify, delete and create pull payments without automatic approval on the selected stores.")},
|
||||
{Policies.CanUseInternalLightningNode, ("Use the internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{Policies.CanViewLightningInvoiceInternalNode, ("View invoices from internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to view BOLT11 invoices.")},
|
||||
{Policies.CanCreateLightningInvoiceInternalNode, ("Create invoices with internal lightning node", "The app will be able to use the internal BTCPay Server lightning node to create BOLT11 invoices.")},
|
||||
{Policies.CanUseLightningNodeInStore, ("Use the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to all your stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{Policies.CanViewLightningInvoiceInStore, ("View the lightning invoices associated with your stores", "The app will be able to view the lightning invoices connected to all your stores.")},
|
||||
{Policies.CanCreateLightningInvoiceInStore, ("Create invoices from the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to all your stores to create BOLT11 invoices.")},
|
||||
{$"{Policies.CanUseLightningNodeInStore}:", ("Use the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to the selected stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{$"{Policies.CanViewLightningInvoiceInStore}:", ("View the lightning invoices associated with your stores", "The app will be able to view the lightning invoices connected to the selected stores.")},
|
||||
{$"{Policies.CanCreateLightningInvoiceInStore}:", ("Create invoices from the lightning nodes associated with your stores", "The app will be able to use the lightning nodes connected to the selected stores to create BOLT11 invoices.")},
|
||||
{Policies.Unrestricted, ("Unrestricted access", "Grants unrestricted access to your account.")},
|
||||
{Policies.CanViewUsers, ("View users", "Allows seeing all users on this server.")},
|
||||
{Policies.CanCreateUser, ("Create new users", "Allows creating new users on this server.")},
|
||||
{Policies.CanManageUsers, ("Manage users", "Allows creating/deleting API keys for users.")},
|
||||
{Policies.CanDeleteUser, ("Delete user", "Allows deleting the user to whom it is assigned. Admin users can delete any user without this permission.")},
|
||||
{Policies.CanModifyStoreSettings, ("Modify your stores", "Allows managing invoices on all your stores and modify their settings.")},
|
||||
{$"{Policies.CanModifyStoreSettings}:", ("Manage selected stores", "Allows managing invoices on the selected stores and modify their settings.")},
|
||||
{Policies.CanViewCustodianAccounts, ("View exchange accounts linked to your stores", "Allows seeing exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanViewCustodianAccounts}:", ("View exchange accounts linked to selected stores", "Allows seeing exchange accounts linked to the selected stores.")},
|
||||
{Policies.CanManageCustodianAccounts, ("Manage exchange accounts linked to your stores", "Allows modifying exchange accounts linked to your stores.")},
|
||||
{$"{Policies.CanManageCustodianAccounts}:", ("Manage exchange accounts linked to selected stores", "Allows modifying exchange accounts linked to selected stores.")},
|
||||
{Policies.CanDepositToCustodianAccounts, ("Deposit funds to exchange accounts linked to your stores", "Allows depositing funds to your exchange accounts.")},
|
||||
{$"{Policies.CanDepositToCustodianAccounts}:", ("Deposit funds to exchange accounts linked to selected stores", "Allows depositing funds to selected store's exchange accounts.")},
|
||||
{Policies.CanWithdrawFromCustodianAccounts, ("Withdraw funds from exchange accounts to your store", "Allows withdrawing funds from your exchange accounts to your store.")},
|
||||
{$"{Policies.CanWithdrawFromCustodianAccounts}:", ("Withdraw funds from selected store's exchange accounts", "Allows withdrawing funds from your selected store's exchange accounts.")},
|
||||
{Policies.CanTradeCustodianAccount, ("Trade funds on your store's exchange accounts", "Allows trading funds on your store's exchange accounts.")},
|
||||
{$"{Policies.CanTradeCustodianAccount}:", ("Trade funds on selected store's exchange accounts", "Allows trading funds on selected store's exchange accounts.")},
|
||||
{Policies.CanModifyStoreWebhooks, ("Modify stores webhooks", "Allows modifying the webhooks of all your stores.")},
|
||||
{$"{Policies.CanModifyStoreWebhooks}:", ("Modify selected stores' webhooks", "Allows modifying the webhooks of the selected stores.")},
|
||||
{Policies.CanViewStoreSettings, ("View your stores", "Allows viewing stores settings.")},
|
||||
{$"{Policies.CanViewStoreSettings}:", ("View your stores", "Allows viewing the selected stores' settings.")},
|
||||
{Policies.CanModifyServerSettings, ("Manage your server", "Grants total control on the server settings of your server.")},
|
||||
{Policies.CanViewProfile, ("View your profile", "Allows viewing your user profile.")},
|
||||
{Policies.CanModifyProfile, ("Manage your profile", "Allows viewing and modifying your user profile.")},
|
||||
{Policies.CanManageNotificationsForUser, ("Manage your notifications", "Allows viewing and modifying your user notifications.")},
|
||||
{Policies.CanViewNotificationsForUser, ("View your notifications", "Allows viewing your user notifications.")},
|
||||
{Policies.CanCreateInvoice, ("Create an invoice", "Allows creating new invoices.")},
|
||||
{$"{Policies.CanCreateInvoice}:", ("Create an invoice", "Allows creating new invoices on the selected stores.")},
|
||||
{Policies.CanViewInvoices, ("View invoices", "Allows viewing invoices.")},
|
||||
{Policies.CanModifyInvoices, ("Modify invoices", "Allows viewing and modifying invoices.")},
|
||||
{$"{Policies.CanViewInvoices}:", ("View invoices", "Allows viewing invoices on the selected stores.")},
|
||||
{$"{Policies.CanModifyInvoices}:", ("Modify invoices", "Allows viewing and modifying invoices on the selected stores.")},
|
||||
{Policies.CanModifyPaymentRequests, ("Modify your payment requests", "Allows viewing, modifying, deleting and creating new payment requests on all your stores.")},
|
||||
{$"{Policies.CanModifyPaymentRequests}:", ("Manage selected stores' payment requests", "Allows viewing, modifying, deleting and creating new payment requests on the selected stores.")},
|
||||
{Policies.CanViewPaymentRequests, ("View your payment requests", "Allows viewing payment requests.")},
|
||||
{$"{Policies.CanViewPaymentRequests}:", ("View your payment requests", "Allows viewing the selected stores' payment requests.")},
|
||||
{Policies.CanManagePullPayments, ("Manage your pull payments", "Allows viewing, modifying, deleting and creating pull payments on all your stores.")},
|
||||
{$"{Policies.CanManagePullPayments}:", ("Manage selected stores' pull payments", "Allows viewing, modifying, deleting and creating pull payments on the selected stores.")},
|
||||
{Policies.CanCreatePullPayments, ("Create pull payments", "Allows creating pull payments on all your stores.")},
|
||||
{$"{Policies.CanCreatePullPayments}:", ("Create pull payments in selected stores", "Allows creating pull payments on the selected stores.")},
|
||||
{Policies.CanCreateNonApprovedPullPayments, ("Create non-approved pull payments", "Allows creating pull payments without automatic approval on all your stores.")},
|
||||
{$"{Policies.CanCreateNonApprovedPullPayments}:", ("Create non-approved pull payments in selected stores", "Allows viewing, modifying, deleting and creating pull payments without automatic approval on the selected stores.")},
|
||||
{Policies.CanUseInternalLightningNode, ("Use the internal lightning node", "Allows using the internal BTCPay Server lightning node to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{Policies.CanViewLightningInvoiceInternalNode, ("View invoices from internal lightning node", "Allows using the internal BTCPay Server lightning node to view BOLT11 invoices.")},
|
||||
{Policies.CanCreateLightningInvoiceInternalNode, ("Create invoices with internal lightning node", "Allows using the internal BTCPay Server lightning node to create BOLT11 invoices.")},
|
||||
{Policies.CanUseLightningNodeInStore, ("Use the lightning nodes associated with your stores", "Allows using the lightning nodes connected to all your stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{Policies.CanViewLightningInvoiceInStore, ("View the lightning invoices associated with your stores", "Allows viewing the lightning invoices connected to all your stores.")},
|
||||
{Policies.CanCreateLightningInvoiceInStore, ("Create invoices from the lightning nodes associated with your stores", "Allows using the lightning nodes connected to all your stores to create BOLT11 invoices.")},
|
||||
{$"{Policies.CanUseLightningNodeInStore}:", ("Use the lightning nodes associated with your stores", "Allows using the lightning nodes connected to the selected stores to create BOLT11 invoices, connect to other nodes, open new channels and pay BOLT11 invoices.")},
|
||||
{$"{Policies.CanViewLightningInvoiceInStore}:", ("View the lightning invoices associated with your stores", "Allows viewing the lightning invoices connected to the selected stores.")},
|
||||
{$"{Policies.CanCreateLightningInvoiceInStore}:", ("Create invoices from the lightning nodes associated with your stores", "Allows using the lightning nodes connected to the selected stores to create BOLT11 invoices.")},
|
||||
};
|
||||
public string Title
|
||||
{
|
||||
|
@ -29,6 +29,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly PullPaymentHostedService _pullPaymentHostedService;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly IEnumerable<IPayoutHandler> _payoutHandlers;
|
||||
private readonly StoreRepository _storeRepository;
|
||||
@ -37,6 +38,7 @@ namespace BTCPayServer.Controllers
|
||||
CurrencyNameTable currencyNameTable,
|
||||
DisplayFormatter displayFormatter,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
IEnumerable<IPayoutHandler> payoutHandlers,
|
||||
StoreRepository storeRepository)
|
||||
@ -48,6 +50,7 @@ namespace BTCPayServer.Controllers
|
||||
_serializerSettings = serializerSettings;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
_storeRepository = storeRepository;
|
||||
_networkProvider = networkProvider;
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
@ -102,6 +105,13 @@ namespace BTCPayServer.Controllers
|
||||
}).ToList()
|
||||
};
|
||||
vm.IsPending &= vm.AmountDue > 0.0m;
|
||||
|
||||
if (_pullPaymentHostedService.SupportsLNURL(blob))
|
||||
{
|
||||
var url = Url.Action("GetLNURLForPullPayment", "UILNURL", new { cryptoCode = _networkProvider.DefaultNetwork.CryptoCode, pullPaymentId = vm.Id }, Request.Scheme, Request.Host.ToString());
|
||||
vm.LnurlEndpoint = url != null ? new Uri(url) : null;
|
||||
}
|
||||
|
||||
return View(nameof(ViewPullPayment), vm);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@ -250,7 +251,7 @@ namespace BTCPayServer.Controllers
|
||||
CryptoCode = cryptoCode,
|
||||
Method = method,
|
||||
SetupRequest = request,
|
||||
Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic),
|
||||
Confirmation = !isImport,
|
||||
Network = network,
|
||||
Source = isImport ? "SeedImported" : "NBXplorerGenerated",
|
||||
IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet,
|
||||
@ -311,7 +312,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var result = await UpdateWallet(vm);
|
||||
|
||||
if (!ModelState.IsValid || !(result is RedirectToActionResult))
|
||||
if (!ModelState.IsValid || result is not RedirectToActionResult)
|
||||
return result;
|
||||
|
||||
if (!isImport)
|
||||
|
@ -141,7 +141,7 @@ namespace BTCPayServer.Controllers
|
||||
"Delete"));
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/roles/{roleId}/delete")]
|
||||
[HttpPost("{storeId}/roles/{role}/delete")]
|
||||
public async Task<IActionResult> DeleteRolePost(
|
||||
string storeId,
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
|
@ -212,7 +212,9 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId,
|
||||
string? labelFilter = null,
|
||||
int skip = 0,
|
||||
int count = 50
|
||||
int count = 50,
|
||||
bool loadTransactions = false,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var paymentMethod = GetDerivationSchemeSettings(walletId);
|
||||
@ -223,25 +225,25 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
// We can't filter at the database level if we need to apply label filter
|
||||
var preFiltering = string.IsNullOrEmpty(labelFilter);
|
||||
var transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null);
|
||||
var walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
var model = new ListTransactionsViewModel { Skip = skip, Count = count };
|
||||
model.Labels.AddRange(
|
||||
(await WalletRepository.GetWalletLabels(walletId))
|
||||
.Select(c => (c.Label, c.Color, ColorPalette.Default.TextColor(c.Color))));
|
||||
|
||||
IList<TransactionHistoryLine>? transactions = null;
|
||||
Dictionary<string, WalletTransactionInfo>? walletTransactionsInfo = null;
|
||||
if (loadTransactions)
|
||||
{
|
||||
transactions = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, preFiltering ? skip : null, preFiltering ? count : null, cancellationToken: cancellationToken);
|
||||
walletTransactionsInfo = await WalletRepository.GetWalletTransactionsInfo(walletId, transactions.Select(t => t.TransactionId.ToString()).ToArray());
|
||||
}
|
||||
|
||||
if (labelFilter != null)
|
||||
{
|
||||
model.PaginationQuery = new Dictionary<string, object> { { "labelFilter", labelFilter } };
|
||||
}
|
||||
if (transactions == null)
|
||||
if (transactions == null || walletTransactionsInfo is null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message =
|
||||
"There was an error retrieving the transactions list. Is NBXplorer configured correctly?"
|
||||
});
|
||||
model.Transactions = new List<ListTransactionsViewModel.TransactionViewModel>();
|
||||
}
|
||||
else
|
||||
@ -1311,7 +1313,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("{walletId}/export")]
|
||||
public async Task<IActionResult> Export(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
string format, string? labelFilter = null)
|
||||
string format, string? labelFilter = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var paymentMethod = GetDerivationSchemeSettings(walletId);
|
||||
if (paymentMethod == null)
|
||||
@ -1319,7 +1321,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
var wallet = _walletProvider.GetWallet(paymentMethod.Network);
|
||||
var walletTransactionsInfoAsync = WalletRepository.GetWalletTransactionsInfo(walletId, (string[]?)null);
|
||||
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation);
|
||||
var input = await wallet.FetchTransactionHistory(paymentMethod.AccountDerivation, cancellationToken: cancellationToken);
|
||||
var walletTransactionsInfo = await walletTransactionsInfoAsync;
|
||||
var export = new TransactionsExport(wallet, walletTransactionsInfo);
|
||||
var res = export.Process(input, format);
|
||||
|
@ -105,16 +105,23 @@ namespace BTCPayServer
|
||||
|
||||
public static decimal RoundUp(decimal value, int precision)
|
||||
{
|
||||
for (int i = 0; i < precision; i++)
|
||||
try
|
||||
{
|
||||
value = value * 10m;
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value * 10m;
|
||||
}
|
||||
value = Math.Ceiling(value);
|
||||
for (int i = 0; i < precision; i++)
|
||||
{
|
||||
value = value / 10m;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
value = Math.Ceiling(value);
|
||||
for (int i = 0; i < precision; i++)
|
||||
catch (OverflowException)
|
||||
{
|
||||
value = value / 10m;
|
||||
return value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddScheduledTask<T>(this IServiceCollection services, TimeSpan every)
|
||||
|
@ -46,6 +46,8 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
public class PullPaymentHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly string[] _lnurlSupportedCurrencies = { "BTC", "SATS" };
|
||||
|
||||
public class CancelRequest
|
||||
{
|
||||
public CancelRequest(string pullPaymentId)
|
||||
@ -337,6 +339,14 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsLNURL(PullPaymentBlob blob)
|
||||
{
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
|
||||
id.PaymentType == LightningPaymentType.Instance &&
|
||||
_networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency);
|
||||
}
|
||||
|
||||
public Task<RateResult> GetRate(PayoutData payout, string explicitRateRule, CancellationToken cancellationToken)
|
||||
{
|
||||
var ppBlob = payout.PullPaymentData?.GetBlob();
|
||||
|
@ -640,8 +640,6 @@ WHERE cte.""Id""=p.""Id""
|
||||
await using var ctx = _DBContextFactory.CreateContext();
|
||||
foreach (var app in await ctx.Apps.Include(data => data.StoreData).AsQueryable().ToArrayAsync())
|
||||
{
|
||||
ViewPointOfSaleViewModel.Item[] items;
|
||||
string newTemplate;
|
||||
switch (app.AppType)
|
||||
{
|
||||
case CrowdfundAppType.AppType:
|
||||
@ -651,13 +649,6 @@ WHERE cte.""Id""=p.""Id""
|
||||
settings1.TargetCurrency = app.StoreData.GetStoreBlob().DefaultCurrency;
|
||||
app.SetSettings(settings1);
|
||||
}
|
||||
items = AppService.Parse(settings1.PerksTemplate);
|
||||
newTemplate = AppService.SerializeTemplate(items);
|
||||
if (settings1.PerksTemplate != newTemplate)
|
||||
{
|
||||
settings1.PerksTemplate = newTemplate;
|
||||
app.SetSettings(settings1);
|
||||
};
|
||||
break;
|
||||
|
||||
case PointOfSaleAppType.AppType:
|
||||
@ -668,13 +659,6 @@ WHERE cte.""Id""=p.""Id""
|
||||
settings2.Currency = app.StoreData.GetStoreBlob().DefaultCurrency;
|
||||
app.SetSettings(settings2);
|
||||
}
|
||||
items = AppService.Parse(settings2.Template);
|
||||
newTemplate = AppService.SerializeTemplate(items);
|
||||
if (settings2.Template != newTemplate)
|
||||
{
|
||||
settings2.Template = newTemplate;
|
||||
app.SetSettings(settings2);
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using NBXplorer.Models;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels
|
||||
|
@ -96,6 +96,7 @@ namespace BTCPayServer.Models
|
||||
public DateTimeOffset StartDate { get; set; }
|
||||
public DateTime LastRefreshed { get; set; }
|
||||
public CurrencyData CurrencyData { get; set; }
|
||||
public Uri LnurlEndpoint { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
public bool AutoApprove { get; set; }
|
||||
|
||||
|
@ -9,6 +9,7 @@ using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Lightning.LndHub;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.InvoicingModels;
|
||||
@ -122,11 +123,16 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new PaymentMethodUnavailableException("Full node not available");
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(LightningTimeout);
|
||||
var client = CreateLightningClient(supportedPaymentMethod, network);
|
||||
|
||||
// LNDhub-compatible implementations might not offer all of GetInfo data.
|
||||
// Skip checks in those cases, see https://github.com/lnbits/lnbits/issues/1182
|
||||
var isLndHub = client is LndHubLightningClient;
|
||||
|
||||
LightningNodeInformation info;
|
||||
try
|
||||
{
|
||||
@ -136,6 +142,10 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
throw new PaymentMethodUnavailableException("The lightning node did not reply in a timely manner");
|
||||
}
|
||||
catch (NotSupportedException) when (isLndHub)
|
||||
{
|
||||
return new NodeInfo[] {};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"Error while connecting to the API: {ex.Message}" +
|
||||
@ -146,9 +156,9 @@ namespace BTCPayServer.Payments.Lightning
|
||||
var nodeInfo = preferOnion != null && info.NodeInfoList.Any(i => i.IsTor == preferOnion)
|
||||
? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray()
|
||||
: info.NodeInfoList.Select(i => i).ToArray();
|
||||
|
||||
|
||||
var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
|
||||
if (blocksGap > 10)
|
||||
if (blocksGap > 10 && !(isLndHub && info.BlockHeight == 0))
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Payments.Lightning
|
||||
@ -10,7 +11,9 @@ namespace BTCPayServer.Payments.Lightning
|
||||
public class LightningLikePaymentMethodDetails : IPaymentMethodDetails
|
||||
{
|
||||
public string BOLT11 { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
public uint256 PaymentHash { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
public uint256 Preimage { get; set; }
|
||||
public string InvoiceId { get; set; }
|
||||
public string NodeInfo { get; set; }
|
||||
|
@ -60,6 +60,11 @@ namespace BTCPayServer.Plugins.PayButton.Controllers
|
||||
}
|
||||
|
||||
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), false, store.Id);
|
||||
// unset app store data, because we don't need it and inclusion leads to circular references when serializing to JSON
|
||||
foreach (var app in apps)
|
||||
{
|
||||
app.App.StoreData = null;
|
||||
}
|
||||
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
|
||||
var model = new PayButtonViewModel
|
||||
{
|
||||
|
@ -131,6 +131,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
public async Task<IActionResult> ViewPointOfSale(string appId,
|
||||
PosViewType? viewType = null,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? amount = null,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? tip = null,
|
||||
[ModelBinder(typeof(InvariantDecimalModelBinder))] decimal? discount = null,
|
||||
string email = null,
|
||||
string orderId = null,
|
||||
string notificationUrl = null,
|
||||
@ -197,15 +199,14 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
if (!settings.ShowCustomAmount && currentView != PosViewType.Cart && currentView != PosViewType.Light)
|
||||
return NotFound();
|
||||
|
||||
price = amount;
|
||||
title = settings.Title;
|
||||
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
|
||||
|
||||
price = amount;
|
||||
if (currentView == PosViewType.Cart &&
|
||||
AppService.TryParsePosCartItems(jposData, out cartItems))
|
||||
{
|
||||
price = 0.0m;
|
||||
choices = AppService.Parse(settings.Template, false);
|
||||
var expectedMinimumAmount = 0m;
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
var itemChoice = choices.FirstOrDefault(c => c.Id == cartItem.Key);
|
||||
@ -229,13 +230,12 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
expectedCartItemPrice = itemChoice.Price ?? 0;
|
||||
}
|
||||
|
||||
expectedMinimumAmount += expectedCartItemPrice * cartItem.Value;
|
||||
}
|
||||
|
||||
if (expectedMinimumAmount > amount)
|
||||
{
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
price += expectedCartItemPrice * cartItem.Value;
|
||||
}
|
||||
if (discount is decimal d)
|
||||
price -= price * d/100.0m;
|
||||
if (tip is decimal t)
|
||||
price += t;
|
||||
}
|
||||
}
|
||||
|
||||
@ -343,10 +343,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
|
||||
if (appPosData.Tip > 0)
|
||||
{
|
||||
receiptData.Add("Tip",
|
||||
$"{_displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)}");
|
||||
receiptData.Add("Tip", _displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
}
|
||||
|
||||
}
|
||||
entity.Metadata.SetAdditionalData("receiptData", receiptData);
|
||||
|
||||
|
@ -2,6 +2,7 @@ namespace BTCPayServer.Security.Greenfield
|
||||
{
|
||||
public static class GreenfieldConstants
|
||||
{
|
||||
public const decimal MaxAmount = ulong.MaxValue;
|
||||
public const string AuthenticationType = "Greenfield";
|
||||
|
||||
public static class ClaimTypes
|
||||
|
@ -176,7 +176,7 @@ namespace BTCPayServer.Services.Apps
|
||||
res.Add(new InvoiceStatsItem
|
||||
{
|
||||
ItemCode = item.Id,
|
||||
FiatPrice = lineItem.Price.Value,
|
||||
FiatPrice = lineItem.Price,
|
||||
Date = e.InvoiceTime.Date
|
||||
});
|
||||
}
|
||||
|
@ -272,6 +272,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
public SpeedPolicy SpeedPolicy { get; set; }
|
||||
public string DefaultLanguage { get; set; }
|
||||
[Obsolete("Use GetPaymentMethod(network) instead")]
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Rate { get; set; }
|
||||
public DateTimeOffset InvoiceTime { get; set; }
|
||||
public DateTimeOffset ExpirationTime { get; set; }
|
||||
@ -280,7 +281,7 @@ namespace BTCPayServer.Services.Invoices
|
||||
public string DepositAddress { get; set; }
|
||||
|
||||
public InvoiceMetadata Metadata { get; set; }
|
||||
|
||||
[JsonConverter(typeof(NumericStringJsonConverter))]
|
||||
public decimal Price { get; set; }
|
||||
public string Currency { get; set; }
|
||||
public string DefaultPaymentMethod { get; set; }
|
||||
@ -1006,6 +1007,11 @@ namespace BTCPayServer.Services.Invoices
|
||||
};
|
||||
}
|
||||
|
||||
// A bug in previous version of BTCPay Server wasn't properly serializing those fields
|
||||
if (PaymentMethodDetails["PaymentHash"] is JObject)
|
||||
PaymentMethodDetails["PaymentHash"] = null;
|
||||
if (PaymentMethodDetails["Preimage"] is JObject)
|
||||
PaymentMethodDetails["Preimage"] = null;
|
||||
IPaymentMethodDetails details = GetId().PaymentType.DeserializePaymentMethodDetails(Network, PaymentMethodDetails.ToString());
|
||||
switch (details)
|
||||
{
|
||||
@ -1072,7 +1078,8 @@ namespace BTCPayServer.Services.Invoices
|
||||
var cryptoPaid = 0.0m;
|
||||
|
||||
int precision = Network?.Divisibility ?? 8;
|
||||
var totalDueNoNetworkCost = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
||||
|
||||
var totalDueNoNetworkCost = Coins(Extensions.RoundUp(totalDue, precision));
|
||||
bool paidEnough = paid >= Extensions.RoundUp(totalDue, precision);
|
||||
int txRequired = 0;
|
||||
decimal networkFeeAlreadyPaid = 0.0m;
|
||||
@ -1107,14 +1114,14 @@ namespace BTCPayServer.Services.Invoices
|
||||
totalDue += GetTxFee();
|
||||
}
|
||||
|
||||
accounting.TotalDue = Money.Coins(Extensions.RoundUp(totalDue, precision));
|
||||
accounting.Paid = Money.Coins(Extensions.RoundUp(paid, precision));
|
||||
accounting.TotalDue = Coins(Extensions.RoundUp(totalDue, precision));
|
||||
accounting.Paid = Coins(Extensions.RoundUp(paid, precision));
|
||||
accounting.TxRequired = txRequired;
|
||||
accounting.CryptoPaid = Money.Coins(Extensions.RoundUp(cryptoPaid, precision));
|
||||
accounting.CryptoPaid = Coins(Extensions.RoundUp(cryptoPaid, precision));
|
||||
accounting.Due = Money.Max(accounting.TotalDue - accounting.Paid, Money.Zero);
|
||||
accounting.DueUncapped = accounting.TotalDue - accounting.Paid;
|
||||
accounting.NetworkFee = accounting.TotalDue - totalDueNoNetworkCost;
|
||||
accounting.NetworkFeeAlreadyPaid = Money.Coins(Extensions.RoundUp(networkFeeAlreadyPaid, precision));
|
||||
accounting.NetworkFeeAlreadyPaid = Coins(Extensions.RoundUp(networkFeeAlreadyPaid, precision));
|
||||
// If the total due is 0, there is no payment tolerance to calculate
|
||||
var minimumTotalDueSatoshi = accounting.TotalDue.Satoshi == 0
|
||||
? 0
|
||||
@ -1124,6 +1131,20 @@ namespace BTCPayServer.Services.Invoices
|
||||
return accounting;
|
||||
}
|
||||
|
||||
const decimal MaxCoinValue = decimal.MaxValue / 1_0000_0000m;
|
||||
private Money Coins(decimal v)
|
||||
{
|
||||
if (v > MaxCoinValue)
|
||||
v = MaxCoinValue;
|
||||
// Clamp the value to not crash on degenerate invoices
|
||||
v *= 1_0000_0000m;
|
||||
if (v > long.MaxValue)
|
||||
return Money.Satoshis(long.MaxValue);
|
||||
if (v < long.MinValue)
|
||||
return Money.Satoshis(long.MinValue);
|
||||
return Money.Satoshis(v);
|
||||
}
|
||||
|
||||
private decimal GetTxFee()
|
||||
{
|
||||
return GetPaymentMethodDetails()?.GetNextNetworkFee() ?? 0m;
|
||||
|
@ -521,8 +521,14 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
invoiceData.Status = legacyStatus.ToLowerInvariant();
|
||||
invoiceData.ExceptionStatus = InvoiceExceptionStatus.Marked.ToString().ToLowerInvariant();
|
||||
_eventAggregator.Publish(new InvoiceEvent(ToEntity(invoiceData), eventName));
|
||||
await context.SaveChangesAsync();
|
||||
try
|
||||
{
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_eventAggregator.Publish(new InvoiceEvent(ToEntity(invoiceData), eventName));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -1,4 +1,3 @@
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -34,7 +33,7 @@ public class PosAppCartItem
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "price")]
|
||||
public PosAppCartItemPrice Price { get; set; }
|
||||
public decimal Price { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "title")]
|
||||
public string Title { get; set; }
|
||||
@ -54,9 +53,6 @@ public class PosAppCartItemPrice
|
||||
[JsonProperty(PropertyName = "formatted")]
|
||||
public string Formatted { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "value")]
|
||||
public decimal Value { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "type")]
|
||||
public ViewPointOfSaleViewModel.ItemPriceType Type { get; set; }
|
||||
}
|
||||
|
@ -215,7 +215,7 @@ namespace BTCPayServer.Services.Wallets
|
||||
return await completionSource.Task;
|
||||
}
|
||||
List<TransactionInformation> dummy = new List<TransactionInformation>();
|
||||
public async Task<IList<TransactionHistoryLine>> FetchTransactionHistory(DerivationStrategyBase derivationStrategyBase, int? skip = null, int? count = null, TimeSpan? interval = null)
|
||||
public async Task<IList<TransactionHistoryLine>> FetchTransactionHistory(DerivationStrategyBase derivationStrategyBase, int? skip = null, int? count = null, TimeSpan? interval = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// This is two paths:
|
||||
// * Sometimes we can ask the DB to do the filtering of rows: If that's the case, we should try to filter at the DB level directly as it is the most efficient.
|
||||
@ -243,18 +243,21 @@ namespace BTCPayServer.Services.Wallets
|
||||
else
|
||||
{
|
||||
await using var ctx = await NbxplorerConnectionFactory.OpenConnection();
|
||||
var rows = await ctx.QueryAsync<(string tx_id, DateTimeOffset seen_at, string blk_id, long? blk_height, long balance_change, string asset_id, long confs)>(
|
||||
"SELECT r.tx_id, r.seen_at, t.blk_id, t.blk_height, r.balance_change, r.asset_id, COALESCE((SELECT height FROM get_tip('BTC')) - t.blk_height + 1, 0) AS confs " +
|
||||
var cmd = new CommandDefinition(
|
||||
commandText: "SELECT r.tx_id, r.seen_at, t.blk_id, t.blk_height, r.balance_change, r.asset_id, COALESCE((SELECT height FROM get_tip('BTC')) - t.blk_height + 1, 0) AS confs " +
|
||||
"FROM get_wallets_recent(@wallet_id, @code, @interval, @count, @skip) r " +
|
||||
"JOIN txs t USING (code, tx_id) " +
|
||||
"ORDER BY r.seen_at DESC", new
|
||||
"ORDER BY r.seen_at DESC",
|
||||
parameters: new
|
||||
{
|
||||
wallet_id = NBXplorer.Client.DBUtils.nbxv1_get_wallet_id(Network.CryptoCode, derivationStrategyBase.ToString()),
|
||||
code = Network.CryptoCode,
|
||||
count = count,
|
||||
skip = skip,
|
||||
interval = interval is TimeSpan t ? t : TimeSpan.FromDays(365 * 1000)
|
||||
});
|
||||
},
|
||||
cancellationToken: cancellationToken);
|
||||
var rows = await ctx.QueryAsync<(string tx_id, DateTimeOffset seen_at, string blk_id, long? blk_height, long balance_change, string asset_id, long confs)>(cmd);
|
||||
rows.TryGetNonEnumeratedCount(out int c);
|
||||
var lines = new List<TransactionHistoryLine>(c);
|
||||
foreach (var row in rows)
|
||||
|
@ -41,7 +41,8 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<input asp-for="Role" required="required" class="form-control" readonly />
|
||||
<input type="hidden" asp-for="Role"/>
|
||||
<input required="required" class="form-control" disabled value="@Model.Role" />
|
||||
}
|
||||
<span asp-validation-for="Role" class="text-danger"></span>
|
||||
</div>
|
||||
|
@ -1,4 +1,3 @@
|
||||
@using BTCPayServer.Models.AppViewModels
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@model BTCPayServer.Plugins.Crowdfund.Models.ContributeToCrowdfund
|
||||
|
||||
@ -28,7 +27,7 @@
|
||||
@(string.IsNullOrEmpty(item.Title) ? item.Id : item.Title)
|
||||
</label>
|
||||
<span class="text-muted">
|
||||
@if (item.Price.Value > 0)
|
||||
@if (item.Price is > 0)
|
||||
{
|
||||
<span>@item.Price.Value</span>
|
||||
<span>@vm.TargetCurrency</span>
|
||||
|
@ -17,7 +17,7 @@
|
||||
<head>
|
||||
<partial name="LayoutHead"/>
|
||||
<partial name="LayoutHeadStoreBranding" model="@(Model.BrandColor, Model.CssFileId, Model.CustomCSSLink, Model.EmbeddedCSS)" />
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.min.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/crowdfund/styles/main.css" asp-append-version="true" rel="stylesheet" />
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
|
||||
@ -81,16 +81,14 @@
|
||||
}
|
||||
@if (Model.EnforceTargetAmount)
|
||||
{
|
||||
<span v-if="srvModel.enforceTargetAmount" class="h5 ms-2">
|
||||
<span v-if="srvModel.enforceTargetAmount" class="h5 ms-2" v-b-tooltip title="No contributions allowed after the goal has been reached">
|
||||
Hardcap Goal
|
||||
<span v-b-tooltip title="No contributions allowed after the goal has been reached"><vc:icon symbol="info" /></span>
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span v-if="!srvModel.enforceTargetAmount" class="h5 ms-2">
|
||||
<span v-if="!srvModel.enforceTargetAmount" class="h5 ms-2" v-b-tooltip title="Contributions allowed even after goal is reached">
|
||||
Softcap Goal
|
||||
<span v-b-tooltip title="Contributions allowed even after goal is reached"><vc:icon symbol="info" /></span>
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
@ -279,7 +277,7 @@
|
||||
<template id="perks-template">
|
||||
<div class="perks-container">
|
||||
<perk v-if="!perks || perks.length === 0"
|
||||
:perk="{title: 'Donate Custom Amount', price: { type: 0, value: null }}"
|
||||
:perk="{title: 'Donate Custom Amount', priceType: 'Topup', price: { type: 'Topup' } }"
|
||||
:target-currency="targetCurrency"
|
||||
:active="active"
|
||||
:loading="loading"
|
||||
@ -393,7 +391,7 @@
|
||||
<script src="~/vendor/moment/moment.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-toasted/vue-toasted.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/bootstrap-vue/bootstrap-vue.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/bootstrap-vue/bootstrap-vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/signalr/signalr.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/animejs/anime.min.js" asp-append-version="true"></script>
|
||||
<script src="~/crowdfund/app.js" asp-append-version="true"></script>
|
||||
|
@ -5,6 +5,7 @@
|
||||
@inject ThemeSettings Theme
|
||||
@inject IFileService FileService
|
||||
|
||||
<script>if (window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true') { document.documentElement.setAttribute('data-hide-sensitive-info', 'true')}</script>
|
||||
@if (Theme.CustomTheme && !string.IsNullOrEmpty(Theme.CssUri))
|
||||
{ // legacy customization with CSS URI - keep it for backwards-compatibility
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(Theme.CssUri)" rel="stylesheet" asp-append-version="true" />
|
||||
@ -25,7 +26,6 @@ else
|
||||
{
|
||||
<link href="~/main/themes/default.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/themes/default-dark.css" asp-append-version="true" rel="stylesheet" id="DarkThemeLinkTag" />
|
||||
<script>if (window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true') { document.documentElement.setAttribute('data-hide-sensitive-info', 'true')}</script>
|
||||
<script src="~/js/theme-switch.js" asp-append-version="true"></script>
|
||||
<noscript><style>.btcpay-theme-switch { display: none !important; }</style></noscript>
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ Vue.component("lnurl-withdraw-checkout", {
|
||||
}
|
||||
},
|
||||
async mounted () {
|
||||
if (!this.supported) return;
|
||||
try {
|
||||
this.permissionGranted = navigator.permissions &&
|
||||
(await navigator.permissions.query({ name: 'nfc' })).state === 'granted'
|
||||
|
@ -197,6 +197,8 @@
|
||||
data-buy
|
||||
>
|
||||
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
|
||||
<input id="js-cart-tip" class="form-control" type="hidden" name="tip">
|
||||
<input id="js-cart-discount" class="form-control" type="hidden" name="discount">
|
||||
<input id="js-cart-posdata" class="form-control" type="hidden" name="posdata">
|
||||
<button id="js-cart-pay" class="btn btn-primary btn-lg" type="submit">
|
||||
<b>@Model.CustomButtonText</b>
|
||||
@ -236,6 +238,10 @@
|
||||
@for (var index = 0; index < Model.Items.Length; index++)
|
||||
{
|
||||
var item = Model.Items[index];
|
||||
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var image = item.Image;
|
||||
var description = item.Description;
|
||||
|
||||
|
@ -4,13 +4,13 @@
|
||||
<form id="app" method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" data-buy v-on:submit="handleFormSubmit" class="d-flex flex-column gap-4 my-auto" v-cloak>
|
||||
<input id="posdata" type="hidden" name="posdata" v-model="posdata">
|
||||
<div ref="display" class="d-flex flex-column align-items-center px-4 mb-auto">
|
||||
<div class="fw-semibold text-muted">{{srvModel.currencyCode}}</div>
|
||||
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }">{{ formatCurrency(total, false) }}</div>
|
||||
<div class="fw-semibold text-muted" id="Currency">{{srvModel.currencyCode}}</div>
|
||||
<div class="fw-bold lh-sm" ref="amount" v-bind:style="{ fontSize: `${fontSize}px` }" id="Amount">{{ formatCurrency(total, false) }}</div>
|
||||
<div class="text-muted text-center mt-2" id="Calculation" v-if="srvModel.showDiscount || srvModel.enableTips">{{ calculation }}</div>
|
||||
</div>
|
||||
<div id="ModeTabs" class="tab-content mb-n2" v-if="srvModel.showDiscount || srvModel.enableTips">
|
||||
<div id="Mode-Discount" class="tab-pane fade px-2" :class="{ show: mode === 'discount', active: mode === 'discount' }" role="tabpanel" aria-labelledby="ModeTablist-Discount" v-if="srvModel.showDiscount">
|
||||
<div class="h4 fw-semibold text-muted text-center">
|
||||
<div class="h4 fw-semibold text-muted text-center" id="Discount">
|
||||
<span class="h3 text-body me-1">{{discountPercent || 0}}%</span> discount
|
||||
</div>
|
||||
</div>
|
||||
@ -18,6 +18,7 @@
|
||||
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-2">
|
||||
<template v-if="srvModel.customTipPercentages">
|
||||
<button
|
||||
id="Tip-Custom"
|
||||
type="button"
|
||||
class="btcpay-pill"
|
||||
:class="{ active: !tipPercent }"
|
||||
@ -30,6 +31,7 @@
|
||||
type="button"
|
||||
class="btcpay-pill"
|
||||
:class="{ active: tipPercent == percentage }"
|
||||
:id="`Tip-${percentage}`"
|
||||
v-on:click.prevent="tipPercentage(percentage)">
|
||||
{{ percentage }}%
|
||||
</button>
|
||||
|
@ -1,4 +1,3 @@
|
||||
@using System.Text.RegularExpressions
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
|
@ -108,6 +108,7 @@
|
||||
<input type="checkbox" id="Disabled" class="btcpay-toggle me-3" v-model="editingItem.disabled" />
|
||||
<label class="form-label mb-0">Disabled</label>
|
||||
</div>
|
||||
<vc:ui-extension-point location="app-template-editor-item-detail" model="Model"></vc:ui-extension-point>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -165,7 +166,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
},
|
||||
save: function(){
|
||||
let template = JSON.stringify(this.items);
|
||||
let template = JSON.stringify(this.items, null, 2);
|
||||
this.getInputElement().val(template);
|
||||
},
|
||||
editItem: function(index){
|
||||
|
@ -116,7 +116,7 @@
|
||||
</div>
|
||||
<button type="button" class="btn btn-link py-0 px-2 mt-2 mb-2 gap-1 add fw-semibold d-inline-flex align-items-center" v-on:click.stop="$emit('add-field', $event, path)">
|
||||
<vc:icon symbol="new" />
|
||||
Add Form Element
|
||||
Add Form Field
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -105,7 +105,9 @@
|
||||
<div id="confetti" v-if="srvModel.celebratePayment" v-on:click="celebratePayment(5000)"></div>
|
||||
<vc:icon symbol="payment-sent" />
|
||||
</span>
|
||||
<h4 v-t="'payment_received'"></h4>
|
||||
<h4 v-t="'payment_received'" class="mb-4"></h4>
|
||||
<p class="text-center" v-t="'payment_received_body'"></p>
|
||||
<p class="text-center" v-if="srvModel.receivedConfirmations !== null && srvModel.requiredConfirmations" v-t="{ path: 'payment_received_confirmations', args: { cryptoCode: realCryptoCode, receivedConfirmations: srvModel.receivedConfirmations, requiredConfirmations: srvModel.requiredConfirmations } }"></p>
|
||||
<div id="PaymentDetails" class="payment-details">
|
||||
<dl class="mb-0">
|
||||
<div>
|
||||
@ -130,8 +132,10 @@
|
||||
<span class="fw-semibold" v-t="'view_details'"></span>
|
||||
<vc:icon symbol="caret-down" />
|
||||
</button>
|
||||
<p class="text-center mt-3" v-t="'payment_received_body'"></p>
|
||||
<p class="text-center" v-if="srvModel.receivedConfirmations !== null && srvModel.requiredConfirmations" v-t="{ path: 'payment_received_confirmations', args: { cryptoCode: realCryptoCode, receivedConfirmations: srvModel.receivedConfirmations, requiredConfirmations: srvModel.requiredConfirmations } }"></p>
|
||||
</div>
|
||||
<div class="buttons mt-3" v-if="storeLink || isModal">
|
||||
<a v-if="storeLink" class="btn btn-secondary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-html="$t('return_to_store', { storeName: srvModel.storeName })" id="StoreLink"></a>
|
||||
<button v-else-if="isModal" class="btn btn-secondary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isSettled" id="settled" key="settled">
|
||||
@ -162,7 +166,7 @@
|
||||
class="mb-5" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="buttons" v-if="srvModel.receiptLink || storeLink || isModal">
|
||||
<a v-if="srvModel.receiptLink" class="btn btn-primary rounded-pill w-100" :href="srvModel.receiptLink" :target="isModal ? '_top' : null" v-t="'view_receipt'" id="ReceiptLink"></a>
|
||||
<a v-if="storeLink" class="btn btn-secondary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-html="$t('return_to_store', { storeName: srvModel.storeName })" id="StoreLink"></a>
|
||||
<button v-else-if="isModal" class="btn btn-secondary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
|
||||
@ -200,7 +204,7 @@
|
||||
</button>
|
||||
<p class="text-center mt-3" v-html="replaceNewlines($t(isPaidPartial ? 'invoice_paidpartial_body' : 'invoice_expired_body', { storeName: srvModel.storeName, minutes: srvModel.maxTimeMinutes }))"></p>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div class="buttons" v-if="(isPaidPartial && srvModel.storeSupportUrl) || storeLink || isModal">
|
||||
<a v-if="isPaidPartial && srvModel.storeSupportUrl" class="btn btn-primary rounded-pill w-100" :href="srvModel.storeSupportUrl" v-t="'contact_us'" id="ContactLink"></a>
|
||||
<a v-if="storeLink" class="btn btn-primary rounded-pill w-100" :href="storeLink" :target="isModal ? '_top' : null" v-html="$t('return_to_store', { storeName: srvModel.storeName })" id="StoreLink"></a>
|
||||
<button v-else-if="isModal" class="btn btn-primary rounded-pill w-100" v-on:click="close" v-t="'Close'"></button>
|
||||
|
@ -71,9 +71,7 @@
|
||||
<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>
|
||||
<a href="?print=true" class="btn btn-link p-0 d-print-none fw-semibold order-1" target="_blank">Print</a>
|
||||
<dd class="text-muted mb-0 fw-semibold">Amount Paid</dd>
|
||||
</div>
|
||||
<dt class="fs-2 mb-0 text-nowrap fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</dt>
|
||||
|
72
BTCPayServer/Views/UIInvoice/InvoiceReceiptPrint.cshtml
Normal file
72
BTCPayServer/Views/UIInvoice/InvoiceReceiptPrint.cshtml
Normal file
@ -0,0 +1,72 @@
|
||||
@model BTCPayServer.Models.InvoicingModels.InvoiceReceiptViewModel
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@{
|
||||
Layout = null;
|
||||
ViewData["Title"] = $"Receipt from {Model.StoreName}";
|
||||
var isProcessing = Model.Status == InvoiceStatus.Processing;
|
||||
var isSettled = Model.Status == InvoiceStatus.Settled;
|
||||
}
|
||||
|
||||
<link href="~/main/bootstrap/bootstrap.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/main/site.css" asp-append-version="true" rel="stylesheet" />
|
||||
|
||||
|
||||
<p class="text-center">@Model.StoreName</p>
|
||||
<p class="text-center">@Model.Timestamp.ToBrowserDate()</p>
|
||||
<p> </p>
|
||||
|
||||
@if (isProcessing)
|
||||
{
|
||||
<div class="lead text-center p-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 p-4 fw-semibold" id="invoice-unsettled">
|
||||
The invoice is not settled.
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h3 class="text-center">
|
||||
<strong>@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</strong>
|
||||
</h3>
|
||||
|
||||
@if (Model.Payments?.Any() is true)
|
||||
{
|
||||
<p> </p>
|
||||
<p class="text-center"><strong>Payments</strong></p>
|
||||
@foreach (var payment in Model.Payments)
|
||||
{
|
||||
<p> </p>
|
||||
<p class="text-center">@payment.Amount <span class="text-nowrap">@payment.PaymentMethod</span></p>
|
||||
<p class="text-center">Rate: @payment.RateFormatted</p>
|
||||
<p class="text-center">= @payment.PaidFormatted</p>
|
||||
}
|
||||
}
|
||||
if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
<p> </p>
|
||||
<p class="text-center"><strong>Additional Data</strong></p>
|
||||
<partial name="PosData" model="(Model.AdditionalData, 1)"/>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(Model.OrderId))
|
||||
{
|
||||
<p> </p>
|
||||
<p class="text-break">Order ID: @Model.OrderId</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.ReceiptOptions.ShowQR is true)
|
||||
{
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
|
||||
}
|
||||
|
||||
<script>window.print();</script>
|
@ -234,7 +234,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="d-flex flex-wrap flex-sm-nowrap align-items-center gap-3 mb-4 @(Model.Invoices.Any() ? "col-xl-7 col-xxl-8" : null)" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
|
||||
<form class="d-flex flex-wrap flex-sm-nowrap align-items-center gap-3 mb-4 @(Model.Invoices.Any() ? "col-xxl-8" : null)" asp-action="ListInvoices" asp-route-storeId="@Model.StoreId" method="get">
|
||||
<input asp-for="Count" type="hidden" />
|
||||
<input asp-for="TimezoneOffset" type="hidden" />
|
||||
<input asp-for="SearchTerm" type="hidden" value="@Model.Search.WithoutSearchText()"/>
|
||||
@ -322,13 +322,13 @@
|
||||
@if (Model.Invoices.Any())
|
||||
{
|
||||
<form method="post" id="MassAction" asp-action="MassAction" class="">
|
||||
<div class="d-inline-flex align-items-center pb-2 float-xl-end mb-2 gap-3">
|
||||
<div class="d-inline-flex align-items-center pb-2 float-xxl-end mb-2 gap-3">
|
||||
<input type="hidden" name="storeId" value="@Model.StoreId" />
|
||||
<div class="dropdown order-xl-1">
|
||||
<div class="dropdown order-xxl-1">
|
||||
<button class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-xl-end" aria-labelledby="ActionsDropdownToggle">
|
||||
<div class="dropdown-menu dropdown-menu-xxl-end" aria-labelledby="ActionsDropdownToggle">
|
||||
<button type="submit" class="dropdown-item" name="command" value="archive" id="ActionsDropdownArchive">Archive</button>
|
||||
@if (HasBooleanFilter("includearchived"))
|
||||
{
|
||||
@ -338,7 +338,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown d-inline-flex align-items-center gap-3">
|
||||
<button class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret order-xl-1" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<button class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret order-xxl-1" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Export
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
|
||||
|
@ -57,7 +57,7 @@
|
||||
<input type="hidden" asp-for="PermissionValues[i].StoreMode" value="@Model.PermissionValues[i].StoreMode" />
|
||||
@if (Model.PermissionValues[i].StoreMode == UIManageController.AddApiKeyViewModel.ApiKeyStoreMode.AllStores)
|
||||
{
|
||||
<div class="form-check">
|
||||
<div class="form-check mb-0">
|
||||
<input id="@Model.PermissionValues[i].Permission" type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-input ms-n4"/>
|
||||
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label me-2 mb-1">
|
||||
<span class="me-lg-1">@Model.PermissionValues[i].Title</span>
|
||||
@ -108,7 +108,7 @@
|
||||
}
|
||||
@if (Model.PermissionValues[i].SpecificStores.Count < Model.Stores.Length)
|
||||
{
|
||||
<div class="mt-3 mb-2">
|
||||
<div class="mt-3">
|
||||
<button type="submit" name="command" value="@($"{Model.PermissionValues[i].Permission}:add-store")" class="btn btn-secondary">Add another store</button>
|
||||
</div>
|
||||
}
|
||||
@ -116,7 +116,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-check">
|
||||
<div class="form-check mb-0">
|
||||
<input id="@Model.PermissionValues[i].Permission" type="checkbox" asp-for="PermissionValues[i].Value" class="form-check-input ms-n4" />
|
||||
<label for="@Model.PermissionValues[i].Permission" class="h5 form-check-label me-2 mb-1">
|
||||
<span class="me-lg-1">@Model.PermissionValues[i].Title</span>
|
||||
|
@ -38,13 +38,13 @@
|
||||
<head>
|
||||
<partial name="LayoutHead" />
|
||||
<partial name="LayoutHeadStoreBranding" model="@(Model.BrandColor, Model.CssFileId, Model.CustomCSSLink, Model.EmbeddedCSS)" />
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.min.css" asp-append-version="true" rel="stylesheet" />
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Safe.Json(Model);
|
||||
</script>
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-toasted/vue-toasted.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/bootstrap-vue/bootstrap-vue.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/bootstrap-vue/bootstrap-vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/signalr/signalr.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/animejs/anime.min.js" asp-append-version="true"></script>
|
||||
<script src="~/payment-request/app.js" asp-append-version="true"></script>
|
||||
|
@ -1,11 +1,9 @@
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Payments
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject BTCPayNetworkProvider BtcPayNetworkProvider
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Models.ViewPullPaymentModel
|
||||
@{
|
||||
@ -25,29 +23,13 @@
|
||||
return "bg-warning";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
string lnurl = null;
|
||||
string lnurlUri = null;
|
||||
|
||||
var pms = Model.PaymentMethods.FirstOrDefault(id => id.PaymentType == LightningPaymentType.Instance && BtcPayNetworkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
if (pms is not null && Model.Currency.Equals(pms.CryptoCode, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var lnurlEndpoint = new Uri(Url.Action("GetLNURLForPullPayment", "UILNURL", new
|
||||
{
|
||||
cryptoCode = pms.CryptoCode,
|
||||
pullPaymentId = Model.Id
|
||||
}, Context.Request.Scheme, Context.Request.Host.ToString()));
|
||||
lnurl = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", true).ToString();
|
||||
lnurlUri = LNURL.LNURL.EncodeUri(lnurlEndpoint, "withdrawRequest", false).ToString();
|
||||
}
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" @(Env.IsDeveloping ? " data-devenv" : "")>
|
||||
<head>
|
||||
<partial name="LayoutHead" />
|
||||
<partial name="LayoutHeadStoreBranding" model="@(Model.BrandColor, Model.CssFileId, Model.CustomCSSLink, Model.EmbeddedCSS)" />
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.css" asp-append-version="true" rel="stylesheet" />
|
||||
<link href="~/vendor/bootstrap-vue/bootstrap-vue.min.css" asp-append-version="true" rel="stylesheet" />
|
||||
<style>
|
||||
.no-marker > ul { list-style-type: none; }
|
||||
</style>
|
||||
@ -62,7 +44,7 @@
|
||||
<div class="row align-items-center" style="width:calc(100% + 30px)">
|
||||
<div class="col-12 mb-3 col-lg-6 mb-lg-0">
|
||||
<div class="input-group">
|
||||
@if (lnurl is not null)
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
<button type="button" class="input-group-prepend btn btn-outline-secondary" id="lnurlwithdraw-button" data-bs-toggle="modal" data-bs-target="#scan-qr-modal">
|
||||
<span class="fa fa-qrcode fa-2x" title="LNURL-Withdraw"></span>
|
||||
@ -223,8 +205,10 @@
|
||||
</footer>
|
||||
</div>
|
||||
<partial name="LayoutFoot" />
|
||||
@if (lnurl is not null)
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
var lnurlUri = LNURL.LNURL.EncodeUri(Model.LnurlEndpoint, "withdrawRequest", false).ToString();
|
||||
var lnurlBech32 = LNURL.LNURL.EncodeUri(Model.LnurlEndpoint, "withdrawRequest", true).ToString();
|
||||
var note = "You can scan or open this link with a <a href='https://github.com/fiatjaf/lnurl-rfc#lnurl-documents' target='_blank' rel='noreferrer noopener'>LNURL-Withdraw</a> enabled wallet.";
|
||||
if (!Model.AutoApprove)
|
||||
{
|
||||
@ -237,7 +221,7 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const modes = {
|
||||
uri: { title: "URI", fragments: [@Safe.Json(lnurlUri)], showData: true, href: @Safe.Json(lnurlUri) },
|
||||
bech32: { title: "Bech32", fragments: [@Safe.Json(lnurl)], showData: true, href: @Safe.Json(lnurl) }
|
||||
bech32: { title: "Bech32", fragments: [@Safe.Json(lnurlBech32)], showData: true, href: @Safe.Json(lnurlBech32) }
|
||||
};
|
||||
initQRShow({ title: "LNURL Withdraw", note: @Safe.Json(note), modes })
|
||||
});
|
||||
|
@ -8,11 +8,11 @@
|
||||
<div class="d-flex align-items-center justify-content-between mt-n1 mb-4">
|
||||
<h3 class="mb-0">@ViewData["Title"]</h3>
|
||||
<a asp-action="storage" asp-route-forceChoice="true" asp-route-returnurl="@ViewData["ReturnUrl"]" class="btn btn-secondary d-flex align-items-center">
|
||||
<vc:icon symbol="settings"/>
|
||||
<vc:icon symbol="settings" />
|
||||
<span class="ms-1">Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@if (!Model.StorageConfigured)
|
||||
{
|
||||
<p>
|
||||
@ -42,66 +42,87 @@
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (Model.DirectUrlByFiles is { Count: > 0 })
|
||||
{
|
||||
foreach (var fileUrlPair in Model.DirectUrlByFiles)
|
||||
{
|
||||
var fileId = fileUrlPair.Key;
|
||||
var file = Model.Files.Single(storedFile => storedFile.Id.Equals(fileId, StringComparison.InvariantCultureIgnoreCase));
|
||||
var url = Url.Action("GetFile", "UIStorage", new { fileId }, Context.Request.Scheme, Context.Request.Host.ToString());
|
||||
<div class="border border-light rounded bg-tile mt-3">
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-4">
|
||||
<div class="input-group">
|
||||
<div class="form-floating">
|
||||
<input id="@fileId-name" class="form-control-plaintext" readonly="readonly" value="@file.FileName">
|
||||
<label>File name</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link" data-clipboard="@file.FileName">
|
||||
<vc:icon symbol="copy" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-4 ">
|
||||
<div class="input-group ">
|
||||
<div class="form-floating">
|
||||
<input id="@fileId" class="form-control-plaintext" readonly="readonly" value="@fileId">
|
||||
<label>File Id</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link" data-clipboard="@fileId">
|
||||
<vc:icon symbol="copy" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" col-sm-12 col-md-4">
|
||||
<div class="input-group">
|
||||
<div class="form-floating">
|
||||
<input id="@fileId-url" class="form-control-plaintext" readonly="readonly" value="@url">
|
||||
<label>Permanent Url</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link" data-clipboard="@url">
|
||||
<vc:icon symbol="copy" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.Files.Any())
|
||||
{
|
||||
<table class="table table-hover table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Timestamp</th>
|
||||
<th>User</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var file in Model.Files)
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>@file.FileName</td>
|
||||
<td>@file.Timestamp.ToBrowserDate()</td>
|
||||
<td>@file.ApplicationUser.UserName</td>
|
||||
<td class="text-end">
|
||||
<a href="@Url.Action("Files", "UIServer", new { fileIds = new string[] { file.Id } })">Get Link</a>
|
||||
- <a asp-action="DeleteFile" asp-route-fileId="@file.Id">Remove</a>
|
||||
</td>
|
||||
<th>Name</th>
|
||||
<th>Timestamp</th>
|
||||
<th>User</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var file in Model.Files)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<a asp-action="Files" asp-route-fileIds="@file.Id">@file.FileName</a>
|
||||
</td>
|
||||
<td>@file.Timestamp.ToBrowserDate()</td>
|
||||
<td>@file.ApplicationUser.UserName</td>
|
||||
<td class="text-end">
|
||||
<a href="@Url.Action("Files", "UIServer", new {fileIds = new [] { file.Id }})" class="text-nowrap">Get Link</a>
|
||||
- <a asp-action="DeleteFile" asp-route-fileId="@file.Id">Remove</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
There are no files yet.
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (Model.DirectUrlByFiles != null && Model.DirectUrlByFiles.Count > 0)
|
||||
{
|
||||
foreach (KeyValuePair<string, string> fileUrlPair in Model.DirectUrlByFiles)
|
||||
{
|
||||
var fileId = fileUrlPair.Key;
|
||||
var file = Model.Files.Single(storedFile => storedFile.Id.Equals(fileId, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
<div class="card mb-2">
|
||||
<div class="card-text">
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
@file.FileName
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<strong>URL:</strong>
|
||||
<a asp-action="GetFile" asp-controller="UIStorage" asp-route-fileId="@fileId" target="_blank">
|
||||
@Url.Action("GetFile", "UIStorage", new
|
||||
{
|
||||
fileId = fileId
|
||||
}, Context.Request.Scheme, Context.Request.Host.ToString())
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-secondary mt-3">There are no files yet.</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
@ -140,7 +140,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.AdditionalOptions is not null)
|
||||
{
|
||||
@foreach (var dictKeys in Model.AdditionalOptions)
|
||||
{
|
||||
<input type="hidden" asp-for="AdditionalOptions[dictKeys.Key]" />
|
||||
}
|
||||
}
|
||||
<button type="submit" class="btn btn-primary" id="Continue">@(isImport ? "Continue" : "Create")</button>
|
||||
</form>
|
||||
|
||||
|
@ -19,3 +19,5 @@
|
||||
}
|
||||
|
||||
@RenderBody()
|
||||
|
||||
<vc:ui-extension-point location="onchain-wallet-setup-post-body" model="@Model"/>
|
||||
|
@ -49,10 +49,10 @@
|
||||
<script src="~/modal/btcpay.js" asp-append-version="true" async></script>
|
||||
@* Custom Range Modal *@
|
||||
<script>
|
||||
let observer = null;
|
||||
let $loadMore = document.getElementById('LoadMore');
|
||||
const $actions = document.getElementById('ListActions');
|
||||
const $transactions = document.getElementById('WalletTransactions');
|
||||
const $list = document.getElementById('WalletTransactionsList');
|
||||
const $dropdowns = document.getElementById('Dropdowns');
|
||||
const $indicator = document.getElementById('LoadingIndicator');
|
||||
|
||||
delegate('click', '#selectAllCheckbox', e => {
|
||||
@ -65,19 +65,15 @@
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
delegate('click', '#LoadMore', async () => {
|
||||
$loadMore.setAttribute('disabled', 'disabled');
|
||||
await loadMoreTransactions();
|
||||
});
|
||||
|
||||
if ($actions && $actions.offsetTop - window.innerHeight > 0) {
|
||||
document.getElementById('GoToTop').classList.remove('d-none');
|
||||
}
|
||||
|
||||
const count = @Safe.Json(Model.Count);
|
||||
const skipInitial = @Safe.Json(Model.Skip);
|
||||
const loadMoreUrl = @Safe.Json(Url.Action("WalletTransactions", new { walletId, labelFilter, skip = Model.Skip, count = Model.Count }));
|
||||
let skip = @Safe.Json(Model.Skip);
|
||||
const loadMoreUrl = @Safe.Json(Url.Action("WalletTransactions", new { walletId, labelFilter, skip = Model.Skip, count = Model.Count, loadTransactions = true }));
|
||||
// The next time we load transactions, skip will become 0
|
||||
let skip = @Safe.Json(Model.Skip) - count;
|
||||
|
||||
async function loadMoreTransactions() {
|
||||
$indicator.classList.remove('d-none');
|
||||
@ -93,38 +89,38 @@
|
||||
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
const responseEmpty = html.trim() === '';
|
||||
$list.insertAdjacentHTML('beforeend', html);
|
||||
skip = skipNext;
|
||||
|
||||
if ($loadMore) {
|
||||
// remove load more button
|
||||
$loadMore.remove();
|
||||
$loadMore = null;
|
||||
|
||||
// switch to infinite scroll mode
|
||||
observer = new IntersectionObserver(async entries => {
|
||||
const { isIntersecting } = entries[0];
|
||||
if (isIntersecting) {
|
||||
await loadMoreTransactions();
|
||||
}
|
||||
}, { rootMargin: '128px' });
|
||||
|
||||
// the actions div marks the end of the list table
|
||||
observer.observe($actions);
|
||||
}
|
||||
|
||||
if (html.trim() === '') {
|
||||
if (responseEmpty) {
|
||||
// in case the response html was empty, remove the observer and stop loading
|
||||
observer.unobserve($actions);
|
||||
}
|
||||
} else if ($loadMore) {
|
||||
$loadMore.removeAttribute('disabled');
|
||||
if (!$transactions.dataset.loaded) {
|
||||
$transactions.dataset.loaded = 'true';
|
||||
// replace table and dropdowns if initial response was empty
|
||||
if (responseEmpty) {
|
||||
$dropdowns.remove();
|
||||
$transactions.innerHTML = '<div class="text-secondary" data-loaded="true">There are no transactions yet.</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$indicator.classList.add('d-none');
|
||||
formatDateTimes(document.getElementById('switchTimeFormat').dataset.mode);
|
||||
initLabelManagers();
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(async entries => {
|
||||
const { isIntersecting } = entries[0];
|
||||
if (isIntersecting) {
|
||||
await loadMoreTransactions();
|
||||
}
|
||||
}, { rootMargin: '128px' });
|
||||
|
||||
// the actions div marks the end of the list table
|
||||
observer.observe($actions);
|
||||
</script>
|
||||
}
|
||||
|
||||
@ -147,80 +143,66 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.Transactions.Any())
|
||||
{
|
||||
<div class="d-inline-flex align-items-center gap-3" id="Dropdowns">
|
||||
<div class="dropdown ms-auto" id="Actions">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="ActionsDropdownToggle">
|
||||
<form id="WalletActions" method="post" asp-action="WalletActions" asp-route-walletId="@walletId">
|
||||
<button id="BumpFee" name="command" type="submit" class="dropdown-item" value="cpfp">Bump fee (CPFP)</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown d-inline-flex align-items-center gap-3" id="Export">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Export
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
|
||||
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="csv" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportCSV">CSV</a>
|
||||
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="json" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportJSON">JSON</a>
|
||||
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="bip329" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportBIP329">Wallet Labels (BIP-329)</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="clear:both"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col table-responsive-md" id="walletTable">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="width:2rem;" class="only-for-js">
|
||||
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
|
||||
</th>
|
||||
<th class="w-150px">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
Date
|
||||
<button type="button" class="btn btn-link p-0 fa fa-clock-o switch-time-format" title="Switch date format" id="switchTimeFormat"></button>
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-start">Label</th>
|
||||
<th>Transaction Id</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="text-end" style="min-width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="WalletTransactionsList">
|
||||
<partial name="_WalletTransactionsList" model="Model" />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<noscript>
|
||||
<vc:pager view-model="Model"/>
|
||||
</noscript>
|
||||
|
||||
<div class="text-center only-for-js d-none" id="LoadingIndicator">
|
||||
<div class="spinner-border spinner-border-sm text-secondary ms-2" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-center gap-3 mb-5 only-for-js" id="ListActions">
|
||||
<button type="button" class="btn btn-secondary d-flex align-items-center" id="LoadMore">
|
||||
Load more
|
||||
<div class="d-inline-flex align-items-center gap-3" id="Dropdowns">
|
||||
<div class="dropdown ms-auto" id="Actions">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="ActionsDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Actions
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary d-none" id="GoToTop">Go to top</button>
|
||||
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="ActionsDropdownToggle">
|
||||
<form id="WalletActions" method="post" asp-action="WalletActions" asp-route-walletId="@walletId">
|
||||
<button id="BumpFee" name="command" type="submit" class="dropdown-item" value="cpfp">Bump fee (CPFP)</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-secondary mt-3">
|
||||
There are no transactions @(string.IsNullOrEmpty(labelFilter) ? "yet" : $"labeled with \"{labelFilter}\"").
|
||||
</p>
|
||||
}
|
||||
<div class="dropdown d-inline-flex align-items-center gap-3" id="Export">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="ExportDropdownToggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Export
|
||||
</button>
|
||||
<div class="dropdown-menu" aria-labelledby="ExportDropdownToggle">
|
||||
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="csv" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportCSV">CSV</a>
|
||||
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="json" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportJSON">JSON</a>
|
||||
<a asp-action="Export" asp-route-walletId="@walletId" asp-route-format="bip329" asp-route-labelFilter="@labelFilter" class="dropdown-item export-link" target="_blank" id="ExportBIP329">Wallet Labels (BIP-329)</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="clear:both"></div>
|
||||
|
||||
<div id="WalletTransactions" class="table-responsive-md">
|
||||
<table class="table table-hover">
|
||||
<thead class="thead-inverse">
|
||||
<tr>
|
||||
<th style="width:2rem;" class="only-for-js">
|
||||
<input id="selectAllCheckbox" type="checkbox" class="form-check-input" />
|
||||
</th>
|
||||
<th class="w-150px">
|
||||
<div class="d-flex align-items-center gap-1">
|
||||
Date
|
||||
<button type="button" class="btn btn-link p-0 fa fa-clock-o switch-time-format" title="Switch date format" id="switchTimeFormat"></button>
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-start">Label</th>
|
||||
<th>Transaction Id</th>
|
||||
<th class="text-end">Amount</th>
|
||||
<th class="text-end" style="min-width:60px"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="WalletTransactionsList">
|
||||
<partial name="_WalletTransactionsList" model="Model" />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<noscript>
|
||||
<vc:pager view-model="Model"/>
|
||||
</noscript>
|
||||
|
||||
<div class="text-center only-for-js d-none" id="LoadingIndicator">
|
||||
<div class="spinner-border spinner-border-sm text-secondary ms-2" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-center gap-3 mb-5 only-for-js" id="ListActions">
|
||||
<button type="button" class="btn btn-secondary d-none" id="GoToTop">Go to top</button>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 mb-0">
|
||||
If BTCPay Server shows you an invalid balance, <a asp-action="WalletRescan" asp-route-walletId="@Context.GetRouteValue("walletId")">rescan your wallet</a>.
|
||||
|
@ -328,6 +328,8 @@ Cart.prototype.updateTip = function(amount) {
|
||||
// Update hidden total amount value to be sent to the checkout page
|
||||
Cart.prototype.updateAmount = function() {
|
||||
$('#js-cart-amount').val(this.getTotal(true));
|
||||
$('#js-cart-tip').val(this.tip);
|
||||
$('#js-cart-discount').val(this.discount);
|
||||
}
|
||||
Cart.prototype.updatePosData = function() {
|
||||
|
||||
@ -690,7 +692,6 @@ Cart.prototype.destroy = function(keepAmount) {
|
||||
} else {
|
||||
this.removeItemAll();
|
||||
}
|
||||
|
||||
localStorage.removeItem(this.getStorageKey('cart'));
|
||||
}
|
||||
|
||||
|
@ -74,11 +74,11 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
},
|
||||
posdata () {
|
||||
const data = {
|
||||
subTotal: this.formatCurrency(this.amountNumeric),
|
||||
total: this.formatCurrency(this.totalNumeric)
|
||||
subTotal: this.amountNumeric,
|
||||
total: this.totalNumeric
|
||||
}
|
||||
if (this.tipNumeric > 0) data.tip = this.formatCurrency(this.tipNumeric)
|
||||
if (this.discountNumeric > 0) data.discountAmount = this.formatCurrency(this.discountNumeric)
|
||||
if (this.tipNumeric > 0) data.tip = this.tipNumeric
|
||||
if (this.discountNumeric > 0) data.discountAmount = this.discountNumeric
|
||||
if (this.discountPercentNumeric > 0) data.discountPercentage = this.discountPercentNumeric
|
||||
return JSON.stringify(data)
|
||||
}
|
||||
@ -138,7 +138,7 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
const currency = this.srvModel.currencyCode;
|
||||
if (currency === 'BTC' || currency === 'SATS') return this.formatCrypto(value, withSymbol);
|
||||
const divisibility = this.srvModel.currencyInfo.divisibility;
|
||||
const locale = currency === 'USD' ? 'en-US' : navigator.language;
|
||||
const locale = this.getLocale(currency);
|
||||
const style = withSymbol ? 'currency' : 'decimal';
|
||||
const opts = { currency, style, maximumFractionDigits: divisibility, minimumFractionDigits: divisibility };
|
||||
try {
|
||||
@ -179,6 +179,14 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
this.tipPercent = this.tipPercent !== percentage
|
||||
? percentage
|
||||
: null;
|
||||
},
|
||||
getLocale(currency) {
|
||||
switch (currency) {
|
||||
case 'USD': return 'en-US';
|
||||
case 'EUR': return 'de-DE';
|
||||
case 'JPY': return 'ja-JP';
|
||||
default: return navigator.language;
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -11,20 +11,20 @@
|
||||
"pay_in_wallet": "Πληρωμή στο πορτοφόλι",
|
||||
"pay_by_nfc": "Πληρωμή μέσω NFC",
|
||||
"pay_by_lnurl": "Πληρωμή με LNURL-Withdraw",
|
||||
"invoice_id": "ID Παραστατικού Πληρωμής",
|
||||
"order_id": "ID Παραγγελίας",
|
||||
"invoice_id": "ID παραστατικού πληρωμής",
|
||||
"order_id": "ID παραγγελίας",
|
||||
"total_price": "Συνολική τιμή",
|
||||
"total_fiat": "Συνολική τιμή σε νόμισμα",
|
||||
"exchange_rate": "Ισοτιμία",
|
||||
"amount_paid": "Πληρωθέν ποσό",
|
||||
"amount_due": "Οφειλόμενο ποσό",
|
||||
"recommended_fee": "Συνιστώμενη Αμοιβή",
|
||||
"recommended_fee": "Συνιστώμενη αμοιβή",
|
||||
"fee_rate": "{{feeRate}} sat/byte",
|
||||
"network_cost": "Κόστος Δικτύου",
|
||||
"network_cost": "Κόστος δικτύου",
|
||||
"tx_count": "{{count}} συναλλαγή",
|
||||
"qr_text": "Σαρώστε τον κωδικό QR ή πατήστε για να αντιγράψετε τη διεύθυνση.",
|
||||
"address": "Διεύθυνση",
|
||||
"lightning": "Αστραπή (Lightning)",
|
||||
"lightning": "Lightning",
|
||||
"payment_link": "Σύνδεσμος πληρωμής",
|
||||
"invoice_paid": "Εξοφλημένο τιμολόγιο",
|
||||
"invoice_expired": "Η τιμολόγηση έληξε",
|
||||
|
@ -3,16 +3,16 @@
|
||||
"code": "el-GR",
|
||||
"currentLanguage": "Ελληνικά",
|
||||
"lang": "Γλώσσα",
|
||||
"Awaiting Payment...": "Αναμονή Πληρωμής...",
|
||||
"Awaiting Payment...": "Αναμονή πληρωμής...",
|
||||
"Pay with": "Πληρώστε με",
|
||||
"Contact and Refund Email": "Email Επικοινωνίας & Επιστροφής Πληρωμής",
|
||||
"Contact_Body": "Παρακαλούμε εισάγετε το email σας παρακάτω. Θα επικοινωνήσουμε μαζί σας σε αυτή τη διεύθυνση ηλεκτρονικής αλληλογραφίας εαν προκύψει κάποιο θέμα με την πληρωμή σας.",
|
||||
"Contact and Refund Email": "Email επικοινωνίας & επιστροφής πληρωμής",
|
||||
"Contact_Body": "Παρακαλούμε εισάγετε το email σας παρακάτω. Θα επικοινωνήσουμε μαζί σας σε αυτή τη διεύθυνση ηλεκτρονικής αλληλογραφίας εάν προκύψει κάποιο θέμα με την πληρωμή σας.",
|
||||
"Your email": "Το email σας",
|
||||
"Continue": "Συνέχεια",
|
||||
"Please enter a valid email address": "Παρακαλούμε εισάγετε μια έγκυρη διεύθυνση email",
|
||||
"Order Amount": "Ποσό Παραγγελίας",
|
||||
"Network Cost": "Κόστος Δικτύου",
|
||||
"Already Paid": "Πληρώθηκαν Ήδη",
|
||||
"Order Amount": "Ποσό παραγγελίας",
|
||||
"Network Cost": "Κόστος δικτύου",
|
||||
"Already Paid": "Πληρώθηκαν ήδη",
|
||||
"Due": "Οφειλόμενα",
|
||||
"Scan": "Σάρωση",
|
||||
"Copy": "Αντιγραφή",
|
||||
@ -34,14 +34,14 @@
|
||||
"InvoiceExpired_Body_1": "Το παρών παραστατικό πληρωμής έχει λήξει. Ένα παραστατικό πληρωμής ισχύει μόνο για {{maxTimeMinutes}} λεπτά.\nΜπορείτε να επιστρέψετε στο {{storeName}} εάν θα θέλατε να υποβάλετε ξανά την πληρωμή σας.",
|
||||
"InvoiceExpired_Body_2": "Εάν επιχειρήσατε να στείλετε την πληρωμή σας, αυτή ακόμη δεν έχει γίνει αποδεκτή απο το δίκτυο. Δέν έχουμε λάβει ακόμη την πληρωμή σας.",
|
||||
"InvoiceExpired_Body_3": "Εάν την λάβουμε αργότερα, είτε θα εκτέλεσουμε την παραγγελία σας ή θα επικοινωνήσουμε μαζί σας για να οργανώσουμε την επιστροφή των χρημάτων σας...",
|
||||
"Invoice ID": "ID Παραστατικού Πληρωμής",
|
||||
"Order ID": "ID Παραγγελίας",
|
||||
"Invoice ID": "ID παραστατικού πληρωμής",
|
||||
"Order ID": "ID παραγγελίας",
|
||||
"Return to StoreName": "Επιστροφή στο {{storeName}}",
|
||||
"This invoice has been paid": "Αυτό το παραστατικό έχει πληρωθεί",
|
||||
"This invoice has been archived": "Αυτό το παραστατικό έχει αρχειοθετηθεί",
|
||||
"Archived_Body": "Παρακαλούμε επικοινωνήστε με το κατάστημα για πληροφορίες σχετικά με την παραγγελία ή εάν χρειάζεστε βοήθεια",
|
||||
"BOLT 11 Invoice": "Παραστατικό BOLT 11",
|
||||
"Node Info": "Πληροφορίες Κόμβου",
|
||||
"Node Info": "Πληροφορίες κόμβου",
|
||||
"txCount": "{{count}} συναλλαγή",
|
||||
"txCount_plural": "{{count}} συναλλαγών",
|
||||
"Pay with CoinSwitch": "Πληρώστε με CoinSwitch",
|
||||
|
@ -157,7 +157,7 @@ h2 svg.icon.icon-info {
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
@media (min-width: 1400px) {
|
||||
#MassAction {
|
||||
margin-top: -4rem;
|
||||
}
|
||||
|
@ -68,7 +68,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WebhookDataCreate"
|
||||
"$ref": "#/components/schemas/WebhookDataCreateResult"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -526,9 +526,6 @@
|
||||
},
|
||||
"WebhookData": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/WebhookDataBase"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -538,10 +535,30 @@
|
||||
"nullable": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/WebhookDataBase"
|
||||
}
|
||||
]
|
||||
},
|
||||
"WebhookDataCreate": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/WebhookDataBase"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "Must be used by the callback receiver to ensure the delivery comes from BTCPay Server. BTCPay Server includes the `BTCPay-Sig` HTTP header, whose format is `sha256=HMAC256(UTF8(webhook's secret), body)`. The pattern to authenticate the webhook is similar to [how to secure webhooks in Github](https://docs.github.com/webhooks/securing/). If left out, null, or empty, the secret will be auto-generated.",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"WebhookDataCreateResult": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/WebhookData"
|
||||
@ -551,8 +568,8 @@
|
||||
"properties": {
|
||||
"secret": {
|
||||
"type": "string",
|
||||
"description": "Must be used by the callback receiver to ensure the delivery comes from BTCPay Server. BTCPay Server includes the `BTCPay-Sig` HTTP header, whose format is `sha256=HMAC256(UTF8(webhook's secret), body)`. The pattern to authenticate the webhook is similar to [how to secure webhooks in Github](https://docs.github.com/webhooks/securing/).",
|
||||
"nullable": false
|
||||
"description": "Must be used by the callback receiver to ensure the delivery comes from BTCPay Server. BTCPay Server includes the `BTCPay-Sig` HTTP header, whose format is `sha256=HMAC256(UTF8(webhook's secret), body)`. The pattern to authenticate the webhook is similar to [how to secure webhooks in Github](https://docs.github.com/webhooks/securing/). Value of the auto-generated or custom secret.",
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -579,11 +596,6 @@
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "The id of the webhook",
|
||||
"nullable": false
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this webhook is enabled or not",
|
||||
|
@ -1,311 +0,0 @@
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .15s linear;
|
||||
}
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* workaround for https://github.com/bootstrap-vue/bootstrap-vue/issues/1560 */
|
||||
/* source: _input-group.scss */
|
||||
|
||||
.input-group > .input-group-prepend > .b-dropdown > .btn,
|
||||
.input-group > .input-group-append:not(:last-child) > .b-dropdown > .btn,
|
||||
.input-group > .input-group-append:last-child > .b-dropdown:not(:last-child):not(.dropdown-toggle) > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .b-dropdown > .btn,
|
||||
.input-group > .input-group-prepend:not(:first-child) > .b-dropdown > .btn,
|
||||
.input-group > .input-group-prepend:first-child > .b-dropdown:not(:first-child) > .btn {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
/* Special styling for type=range and type=color input */
|
||||
input.form-control[type="range"],
|
||||
input.form-control[type="color"] {
|
||||
height: 2.25rem;
|
||||
}
|
||||
input.form-control.form-control-sm[type="range"],
|
||||
input.form-control.form-control-sm[type="color"] {
|
||||
height: 1.9375rem;
|
||||
}
|
||||
input.form-control.form-control-lg[type="range"],
|
||||
input.form-control.form-control-lg[type="color"] {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
/* Less padding on type=color */
|
||||
input.form-control[type="color"] {
|
||||
padding: 0.25rem 0.25rem;
|
||||
}
|
||||
input.form-control.form-control-sm[type="color"] {
|
||||
padding: 0.125rem 0.125rem;
|
||||
}
|
||||
|
||||
/* Add support for fixed layout table */
|
||||
table.b-table.b-table-fixed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* Busy table styling */
|
||||
table.b-table[aria-busy='false'] {
|
||||
opacity: 1;
|
||||
}
|
||||
table.b-table[aria-busy='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Sort styling */
|
||||
table.b-table > thead > tr > th,
|
||||
table.b-table > tfoot > tr > th {
|
||||
position: relative;
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting,
|
||||
table.b-table > tfoot > tr > th.sorting {
|
||||
padding-right: 1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting::before,
|
||||
table.b-table > thead > tr > th.sorting::after,
|
||||
table.b-table > tfoot > tr > th.sorting::before,
|
||||
table.b-table > tfoot > tr > th.sorting::after {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: block;
|
||||
opacity: 0.4;
|
||||
padding-bottom: inherit;
|
||||
font-size: inherit;
|
||||
line-height: 180%;
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting::before,
|
||||
table.b-table > tfoot > tr > th.sorting::before {
|
||||
right: 0.75em;
|
||||
content: '\2191';
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting::after,
|
||||
table.b-table > tfoot > tr > th.sorting::after {
|
||||
right: 0.25em;
|
||||
content: '\2193';
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting_asc::after,
|
||||
table.b-table > thead > tr > th.sorting_desc::before,
|
||||
table.b-table > tfoot > tr > th.sorting_asc::after,
|
||||
table.b-table > tfoot > tr > th.sorting_desc::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Stacked table layout */
|
||||
/* Derived from http://blog.adrianroselli.com/2017/11/a-responsive-accessible-table.html */
|
||||
/* Always stacked */
|
||||
table.b-table.b-table-stacked {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked,
|
||||
table.b-table.b-table-stacked > tbody,
|
||||
table.b-table.b-table-stacked > tbody > tr,
|
||||
table.b-table.b-table-stacked > tbody > tr > td,
|
||||
table.b-table.b-table-stacked > tbody > tr > th,
|
||||
table.b-table.b-table-stacked > caption {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked > thead,
|
||||
table.b-table.b-table-stacked > tfoot,
|
||||
table.b-table.b-table-stacked > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@media all and (max-width: 575.99px) {
|
||||
/* Under SM */
|
||||
table.b-table.b-table-stacked-sm {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked-sm,
|
||||
table.b-table.b-table-stacked-sm > tbody,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > td,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > th,
|
||||
table.b-table.b-table-stacked-sm > caption {
|
||||
display: block;
|
||||
}
|
||||
/* hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked-sm > thead,
|
||||
table.b-table.b-table-stacked-sm > tfoot,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 767.99px) {
|
||||
/* under MD */
|
||||
table.b-table.b-table-stacked-md {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked-md,
|
||||
table.b-table.b-table-stacked-md > tbody,
|
||||
table.b-table.b-table-stacked-md > tbody > tr,
|
||||
table.b-table.b-table-stacked-md > tbody > tr > td,
|
||||
table.b-table.b-table-stacked-md > tbody > tr > th,
|
||||
table.b-table.b-table-stacked-md > caption {
|
||||
display: block;
|
||||
}
|
||||
/* hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked-md > thead,
|
||||
table.b-table.b-table-stacked-md > tfoot,
|
||||
table.b-table.b-table-stacked-md > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked-md > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked-md > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked-md > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked-md > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 991.99px) {
|
||||
/* under LG */
|
||||
table.b-table.b-table-stacked-lg {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked-lg,
|
||||
table.b-table.b-table-stacked-lg > tbody,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > td,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > th,
|
||||
table.b-table.b-table-stacked-lg > caption {
|
||||
display: block;
|
||||
}
|
||||
/* hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked-lg > thead,
|
||||
table.b-table.b-table-stacked-lg > tfoot,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 1199.99px) {
|
||||
/* under XL */
|
||||
table.b-table.b-table-stacked-xl {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked-xl,
|
||||
table.b-table.b-table-stacked-xl > tbody,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > td,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > th,
|
||||
table.b-table.b-table-stacked-xl > caption {
|
||||
display: block;
|
||||
}
|
||||
/* hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked-xl > thead,
|
||||
table.b-table.b-table-stacked-xl > tfoot,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/* Details row styling */
|
||||
table.b-table > tbody > tr.b-table-details > td {
|
||||
border-top: none;
|
||||
}
|
16701
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.js
vendored
16701
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.js
vendored
File diff suppressed because it is too large
Load Diff
4
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.min.css
vendored
Normal file
4
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.min.js
vendored
Normal file
11
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.min.min.css.map
vendored
Normal file
1
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.min.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
1
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.min.min.js.map
vendored
Normal file
1
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.min.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.10.0</Version>
|
||||
<Version>1.10.3</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
52
Changelog.md
52
Changelog.md
@ -1,5 +1,57 @@
|
||||
# Changelog
|
||||
|
||||
## 1.10.3
|
||||
|
||||
### New Features
|
||||
|
||||
* Pull Payment: Support LNURL Withdraw with SATS denomination (#5041) @dennnisreimann
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Crowdfund: Fix JS errors in empty state (#5121) @dennisreimann
|
||||
* The current preimage of a invoice's lightning payment method should be available via API (#5111) @NicolasDorier
|
||||
* Dashboard: Limit "Top Items" to five (#5110) @dennisreimann
|
||||
* ToolTip causes glitching when scrolling down on mobile (#4956) @dennisreimann
|
||||
* LN payments failed to be detected on litd (#5104) @NicolasDorier
|
||||
* Fix for LNDHub via LNbits integration (#5083 #4482) @dennisreimann
|
||||
* Applying a discount in PoS with cart wasn't working (#5079) @NicolasDorier
|
||||
* Refund: Fix overpaid option (#5076 #5066) @dennisreimann
|
||||
* Do not crash when an invoice have an amount that is too big (#5070) @NicolasDorier
|
||||
* NFC: Do not start scanning if unsupported (#5067) @dennisreimann
|
||||
* Fix: Incorrect rounding in the receipt of PoS invoice (#5071 #5072) @NicolasDorier
|
||||
* Crowdfund: Fix null pointer exception for topup type (missing price) (#5068) @dennisreimann
|
||||
* Greenfield: Default currency missing from stores API (#5126) @dennisreimann
|
||||
|
||||
### Improvements
|
||||
|
||||
* Load wallet transaction list asynchronously to avoid timeout on large wallets (#5100 #4987) @NicolasDorier
|
||||
* Receipt improvements (#5077) @dennisreimann
|
||||
* Dashboard: Make invoice badges consistent with those on invoices list (#5108 #4969) @dennisreimann
|
||||
* Make file management UI more useful (#5081) @Kukks
|
||||
* After changing PoS items values, the JSON template should be indented @NicolasDorier
|
||||
* Add extension point to template editor (#5080) @Kukks
|
||||
* Querying a lightning address no longer generates an invoice each time (#5117) @NicolasDorier
|
||||
|
||||
## 1.10.2
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix: Stale data when fetching invoice after webhook (#5049) @Kukks
|
||||
* Fix: Crash on migation of old instances (#5051) @NicolasDorier
|
||||
* Fix: Hide sensitive info feature not working with custom theme (#5044) @dennisreimann
|
||||
* Fix: Pay button not rendering on the invoice page (#5043) @dennisreimann
|
||||
* Doc update: Remove id from create webhook endpoint; fix consistency. (#5045) @ndeet
|
||||
|
||||
## 1.10.1
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Point of Sale bug after filling out form Shop + cart (#5031) @Kukks
|
||||
|
||||
### Improvements
|
||||
|
||||
* Language translation update for el-GR
|
||||
|
||||
## 1.10.0
|
||||
|
||||
Notice: Due to the substantial disk space consumption, we are removing all data pertaining to past webhook deliveries (#5005).
|
||||
|
Reference in New Issue
Block a user