Compare commits
73 Commits
v1.10.0-rc
...
igiownq
Author | SHA1 | Date | |
---|---|---|---|
43a59edc80 | |||
983b8c1f54 | |||
d666d8ea1a | |||
3ed81c3a78 | |||
4afec2e2b6 | |||
db83d238d5 | |||
fdcf7b3b7a | |||
53aafcf86b | |||
aec84f6d67 | |||
01e9f82d24 | |||
2eff45e65c | |||
13203c3e2b | |||
82c5e0e43d | |||
a1575f404b | |||
e1509506dc | |||
0c1d0d7b05 | |||
ad70856af0 | |||
e0e21005b9 | |||
b28ba9ff57 | |||
3643e96898 | |||
7c00ab0f18 | |||
8996f4a9bb | |||
4dce9de7fc | |||
8615f120ce | |||
0d0477d661 | |||
b31dc30878 | |||
ede8444c30 | |||
6e392f4cfb | |||
d17fce1137 | |||
e687408d75 | |||
cc3bdc331e | |||
76faf77a1c | |||
d8c0e5bf3a | |||
28c4c320cc | |||
e81403ec3f | |||
1b8810cdb9 | |||
f11424f73a | |||
fa8b977016 | |||
d181846339 | |||
1956919886 | |||
0f66498965 | |||
918cd152b1 | |||
d3222df396 | |||
a84ffd8c7e | |||
6d0f9120b8 | |||
aafb4a7f2a | |||
ae432ff237 | |||
cdc318c71a | |||
94d1cec8a9 | |||
c0bc19ea59 | |||
6f07714cd9 | |||
a9d2cac23c | |||
693b46126b | |||
bbff9710bf | |||
358e122775 | |||
3818468932 | |||
3d2554fbe1 | |||
4309603317 | |||
f733c9ea77 | |||
775ee01171 | |||
33ec790137 | |||
0c575c888c | |||
24f7e51e3a | |||
0a0cf97c55 | |||
16b988d097 | |||
5edc0ff6ef | |||
375b96e508 | |||
1e72b12074 | |||
4a6d52f78e | |||
35dd580e74 | |||
79836ef1de | |||
8cb06f9c6c | |||
215a36e7a9 |
@ -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;
|
||||
@ -961,11 +962,13 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1) .btn-primary")).Click();
|
||||
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
|
||||
s.Driver.FindElement(By.Id("EditorCategories-ts-control")).SendKeys("Drinks");
|
||||
s.Driver.FindElement(By.Id("SaveItemChanges")).Click();
|
||||
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);
|
||||
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
@ -979,6 +982,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
|
||||
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
|
||||
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")).Count);
|
||||
|
||||
var drinks = s.Driver.FindElement(By.CssSelector("label[for='Category-Drinks']"));
|
||||
Assert.Equal("Drinks", drinks.Text);
|
||||
drinks.Click();
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")));
|
||||
s.Driver.FindElement(By.CssSelector("label[for='Category-*']")).Click();
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".card-deck .card:not(.d-none)")).Count);
|
||||
|
||||
s.Driver.Url = posBaseUrl + "/static";
|
||||
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
|
||||
@ -1441,7 +1452,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 +1503,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 +1631,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 +1760,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 +1943,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 +2011,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 +2086,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 +2390,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 +2408,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 +2416,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 +2424,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.25" />
|
||||
<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;
|
||||
|
@ -23,15 +23,17 @@
|
||||
@if (Model.Balance.OffchainBalance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> in channels
|
||||
</span>
|
||||
<div class="d-flex align-items-baseline gap-1">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> in channels
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="balance-details collapse" id="balanceDetailsOffchain">
|
||||
@if (Model.Balance.OffchainBalance.Opening != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Opening" data-sensitive>
|
||||
@Model.Balance.OffchainBalance.Opening
|
||||
</span>
|
||||
@ -42,7 +44,7 @@
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Local != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Local" data-sensitive>
|
||||
@Model.Balance.OffchainBalance.Local
|
||||
</span>
|
||||
@ -53,7 +55,7 @@
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Remote != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Remote" data-sensitive>
|
||||
@Model.Balance.OffchainBalance.Remote
|
||||
</span>
|
||||
@ -64,7 +66,7 @@
|
||||
}
|
||||
@if (Model.Balance.OffchainBalance.Closing != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Closing" data-sensitive>
|
||||
@Model.Balance.OffchainBalance.Closing
|
||||
</span>
|
||||
@ -79,14 +81,16 @@
|
||||
@if (Model.Balance.OnchainBalance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> on-chain
|
||||
</span>
|
||||
<div class="d-flex align-items-baseline gap-1">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
|
||||
<span class="text-secondary fw-semibold text-nowrap">
|
||||
<span class="currency">@Model.CryptoCode</span> on-chain
|
||||
</span>
|
||||
</div>
|
||||
<div class="balance-details collapse" id="balanceDetailsOnchain">
|
||||
@if (Model.Balance.OnchainBalance.Confirmed != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Confirmed" data-sensitive>
|
||||
@Model.Balance.OnchainBalance.Confirmed
|
||||
</span>
|
||||
@ -97,7 +101,7 @@
|
||||
}
|
||||
@if (Model.Balance.OnchainBalance.Unconfirmed != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Unconfirmed" data-sensitive>
|
||||
@Model.Balance.OnchainBalance.Unconfirmed
|
||||
</span>
|
||||
@ -108,7 +112,7 @@
|
||||
}
|
||||
@if (Model.Balance.OnchainBalance.Reserved != null)
|
||||
{
|
||||
<div class="mt-2">
|
||||
<div class="mt-2 d-flex align-items-baseline gap-1">
|
||||
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Reserved" data-sensitive>
|
||||
@Model.Balance.OnchainBalance.Reserved
|
||||
</span>
|
||||
|
@ -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
|
||||
|
@ -3,7 +3,6 @@
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Services
|
||||
@inject SignInManager<ApplicationUser> SignInManager
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject IFileService FileService
|
||||
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
|
||||
@ -33,8 +32,7 @@
|
||||
else
|
||||
{
|
||||
<a asp-controller="UIStores" asp-action="Dashboard" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
}
|
||||
@if (Model.Options.Any())
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -38,12 +39,14 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
.FirstOrDefault()?
|
||||
.Network.CryptoCode;
|
||||
var walletId = cryptoCode != null ? new WalletId(store.Id, cryptoCode) : null;
|
||||
var role = store.GetStoreRoleOfUser(userId);
|
||||
return new StoreSelectorOption
|
||||
{
|
||||
Text = store.StoreName,
|
||||
Value = store.Id,
|
||||
Selected = store.Id == currentStore?.Id,
|
||||
WalletId = walletId
|
||||
WalletId = walletId,
|
||||
IsOwner = role != null && role.Permissions.Contains(Policies.CanModifyStoreSettings)
|
||||
};
|
||||
})
|
||||
.OrderBy(s => s.Text)
|
||||
|
@ -17,7 +17,7 @@
|
||||
<header class="mb-3">
|
||||
@if (Model.Balance != null)
|
||||
{
|
||||
<div class="balance">
|
||||
<div class="balance d-flex align-items-baseline gap-1">
|
||||
<h3 class="d-inline-block me-1" data-balance="@Model.Balance" data-sensitive>@Model.Balance</h3>
|
||||
<span class="text-secondary fw-semibold currency">@Model.CryptoCode</span>
|
||||
</div>
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,6 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.S3.Transfer;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
@ -167,9 +166,17 @@ namespace BTCPayServer.Controllers
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
{
|
||||
await storeRepository.SetDefaultRole(role);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
|
||||
var resolved = await storeRepository.ResolveStoreRoleId(null, role);
|
||||
if (resolved is null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Role could not be set as default";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
await storeRepository.SetDefaultRole(role);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(ListRoles));
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -111,12 +111,6 @@ namespace BTCPayServer.Hosting
|
||||
settings.DeprecatedLightningConnectionStringCheck = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.UnreachableStoreCheck)
|
||||
{
|
||||
await UnreachableStoreCheck();
|
||||
settings.UnreachableStoreCheck = true;
|
||||
await _Settings.UpdateSetting(settings);
|
||||
}
|
||||
if (!settings.ConvertMultiplierToSpread)
|
||||
{
|
||||
await ConvertMultiplierToSpread();
|
||||
@ -646,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:
|
||||
@ -657,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:
|
||||
@ -674,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;
|
||||
}
|
||||
}
|
||||
@ -1068,11 +1046,6 @@ retry:
|
||||
}
|
||||
}
|
||||
|
||||
private Task UnreachableStoreCheck()
|
||||
{
|
||||
return _StoreRepository.CleanUnreachableStores();
|
||||
}
|
||||
|
||||
private async Task DeprecatedLightningConnectionStringCheck()
|
||||
{
|
||||
using var ctx = _DBContextFactory.CreateContext();
|
||||
|
@ -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,17 +230,17 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
var store = await _appService.GetStore(app);
|
||||
var storeBlob = store.GetStoreBlob();
|
||||
var posFormId = settings.FormId;
|
||||
var formData = await FormDataService.GetForm(posFormId);
|
||||
|
||||
@ -297,7 +298,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
RedirectAutomatically = settings.RedirectAutomatically,
|
||||
SupportedTransactionCurrencies = paymentMethods,
|
||||
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
|
||||
? store.GetStoreBlob().RequiresRefundEmail
|
||||
? storeBlob.RequiresRefundEmail
|
||||
: requiresRefundEmail == RequiresRefundEmail.On,
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot(),
|
||||
new List<string> { AppService.GetAppInternalTag(appId) },
|
||||
@ -342,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);
|
||||
|
||||
@ -355,6 +354,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
meta.Merge(formResponseJObject);
|
||||
entity.Metadata = InvoiceMetadata.FromJObject(meta);
|
||||
});
|
||||
if (price is 0 && storeBlob.ReceiptOptions?.Enabled is true)
|
||||
{
|
||||
return RedirectToAction(nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id });
|
||||
}
|
||||
catch (BitpayHttpException e)
|
||||
|
@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -24,6 +27,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public string Description { get; set; }
|
||||
public string Id { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string[] Categories { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Image { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public ItemPriceType PriceType { get; set; }
|
||||
@ -63,7 +68,35 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public bool EnableTips { get; set; }
|
||||
public string Step { get; set; }
|
||||
public string Title { get; set; }
|
||||
public Item[] Items { get; set; }
|
||||
Item[] _Items;
|
||||
public Item[] Items
|
||||
{
|
||||
get
|
||||
{
|
||||
return _Items;
|
||||
}
|
||||
set
|
||||
{
|
||||
_Items = value;
|
||||
UpdateGroups();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateGroups()
|
||||
{
|
||||
AllCategories = null;
|
||||
if (Items is null)
|
||||
return;
|
||||
var groups = Items.SelectMany(g => g.Categories ?? Array.Empty<string>())
|
||||
.ToHashSet()
|
||||
.Select(o => new KeyValuePair<string, string>(o, o))
|
||||
.ToList();
|
||||
if (groups.Count == 0)
|
||||
return;
|
||||
groups.Insert(0, new KeyValuePair<string, string>("All items", "*"));
|
||||
AllCategories = new SelectList(groups, "Value", "Key", "*");
|
||||
}
|
||||
|
||||
public string CurrencyCode { get; set; }
|
||||
public string CurrencySymbol { get; set; }
|
||||
public string AppId { get; set; }
|
||||
@ -76,6 +109,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
||||
public string CustomCSSLink { get; set; }
|
||||
public string CustomLogoLink { get; set; }
|
||||
public string Description { get; set; }
|
||||
public SelectList AllCategories { get; set; }
|
||||
[Display(Name = "Custom CSS Code")]
|
||||
public string EmbeddedCSS { get; set; }
|
||||
public RequiresRefundEmail RequiresRefundEmail { get; set; } = RequiresRefundEmail.InheritFromStore;
|
||||
|
@ -51,7 +51,6 @@ namespace BTCPayServer.Plugins.PointOfSale
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
private readonly HtmlSanitizer _htmlSanitizer;
|
||||
public const string AppType = "PointOfSale";
|
||||
|
||||
public PointOfSaleAppType(
|
||||
@ -65,7 +64,6 @@ namespace BTCPayServer.Plugins.PointOfSale
|
||||
_linkGenerator = linkGenerator;
|
||||
_btcPayServerOptions = btcPayServerOptions;
|
||||
_displayFormatter = displayFormatter;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
}
|
||||
|
||||
public override Task<string> ConfigureLink(AppData app)
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -34,44 +34,44 @@ namespace BTCPayServer.Services.Apps
|
||||
new()
|
||||
{
|
||||
Id = "rooibos",
|
||||
Title = "Rooibos",
|
||||
Title = "Rooibos (limited)",
|
||||
Description =
|
||||
"Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.",
|
||||
Image = "~/img/pos-sample/rooibos.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed,
|
||||
Price = 1.2m
|
||||
Price = 1.2m,
|
||||
Inventory = 5,
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "pu-erh",
|
||||
Title = "Pu Erh",
|
||||
Title = "Pu Erh (free)",
|
||||
Description =
|
||||
"This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.",
|
||||
Image = "~/img/pos-sample/pu-erh.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed,
|
||||
Price = 2
|
||||
Price = 0
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "herbal-tea",
|
||||
Title = "Herbal Tea",
|
||||
Title = "Herbal Tea (minimum)",
|
||||
Description =
|
||||
"Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!",
|
||||
Image = "~/img/pos-sample/herbal-tea.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Minimum,
|
||||
Price = 1.8m,
|
||||
Disabled = true
|
||||
Disabled = false
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = "fruit-tea",
|
||||
Title = "Fruit Tea",
|
||||
Title = "Fruit Tea (any amount)",
|
||||
Description =
|
||||
"The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!",
|
||||
Image = "~/img/pos-sample/fruit-tea.jpg",
|
||||
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Topup,
|
||||
Inventory = 5,
|
||||
Disabled = true
|
||||
Disabled = false
|
||||
}
|
||||
});
|
||||
DefaultView = PosViewType.Static;
|
||||
|
@ -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; }
|
||||
@ -834,20 +835,13 @@ namespace BTCPayServer.Services.Invoices
|
||||
|
||||
public bool CanMarkComplete()
|
||||
{
|
||||
return (Status == InvoiceStatusLegacy.Paid) ||
|
||||
(Status == InvoiceStatusLegacy.New) ||
|
||||
((Status == InvoiceStatusLegacy.New || Status == InvoiceStatusLegacy.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
|
||||
((Status == InvoiceStatusLegacy.New || Status == InvoiceStatusLegacy.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
|
||||
(Status != InvoiceStatusLegacy.Complete && ExceptionStatus == InvoiceExceptionStatus.Marked) ||
|
||||
(Status == InvoiceStatusLegacy.Invalid);
|
||||
return Status is InvoiceStatusLegacy.New or InvoiceStatusLegacy.Paid or InvoiceStatusLegacy.Expired or InvoiceStatusLegacy.Invalid ||
|
||||
(Status != InvoiceStatusLegacy.Complete && ExceptionStatus == InvoiceExceptionStatus.Marked);
|
||||
}
|
||||
|
||||
public bool CanMarkInvalid()
|
||||
{
|
||||
return (Status == InvoiceStatusLegacy.Paid) ||
|
||||
(Status == InvoiceStatusLegacy.New) ||
|
||||
((Status == InvoiceStatusLegacy.New || Status == InvoiceStatusLegacy.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
|
||||
((Status == InvoiceStatusLegacy.New || Status == InvoiceStatusLegacy.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
|
||||
return Status is InvoiceStatusLegacy.New or InvoiceStatusLegacy.Paid or InvoiceStatusLegacy.Expired ||
|
||||
(Status != InvoiceStatusLegacy.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked);
|
||||
}
|
||||
|
||||
@ -1013,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)
|
||||
{
|
||||
@ -1079,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;
|
||||
@ -1114,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
|
||||
@ -1131,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; }
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ namespace BTCPayServer.Services
|
||||
[JsonProperty("MigrateHotwalletProperty2")]
|
||||
public bool MigrateHotwalletProperty { get; set; }
|
||||
public bool MigrateU2FToFIDO2 { get; set; }
|
||||
public bool UnreachableStoreCheck { get; set; }
|
||||
public bool DeprecatedLightningConnectionStringCheck { get; set; }
|
||||
public bool ConvertMultiplierToSpread { get; set; }
|
||||
public bool ConvertNetworkFeeProperty { get; set; }
|
||||
|
@ -3,6 +3,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.S3;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
@ -229,17 +230,19 @@ namespace BTCPayServer.Services.Stores
|
||||
/// <param name="storeId"></param>
|
||||
/// <param name="role"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<StoreRoleId?> ResolveStoreRoleId(string storeId, string? role)
|
||||
public async Task<StoreRoleId?> ResolveStoreRoleId(string? storeId, string? role)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(role))
|
||||
return null;
|
||||
if (role.Contains("::", StringComparison.OrdinalIgnoreCase) || storeId.Contains("::", StringComparison.OrdinalIgnoreCase))
|
||||
if (storeId?.Contains("::", StringComparison.OrdinalIgnoreCase) is true)
|
||||
return null;
|
||||
var roleId = StoreRoleId.Parse(role);
|
||||
if (roleId.StoreId != null && roleId.StoreId != storeId)
|
||||
return null;
|
||||
if ((await GetStoreRole(roleId)) != null)
|
||||
return roleId;
|
||||
if (string.IsNullOrEmpty(storeId))
|
||||
return null;
|
||||
if (roleId.IsServerRole)
|
||||
roleId = new StoreRoleId(storeId, role);
|
||||
if ((await GetStoreRole(roleId)) != null)
|
||||
|
@ -157,8 +157,6 @@ namespace BTCPayServer.Services
|
||||
{
|
||||
_logger.LogError($"Failed to delete user {user.Id}");
|
||||
}
|
||||
|
||||
await _storeRepository.CleanUnreachableStores();
|
||||
}
|
||||
|
||||
|
||||
|
@ -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>
|
||||
@ -38,7 +37,7 @@
|
||||
@Safe.Raw("or more")
|
||||
}
|
||||
}
|
||||
else if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed )
|
||||
else if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup )
|
||||
{
|
||||
@Safe.Raw("Any amount")
|
||||
}else if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed)
|
||||
|
@ -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"
|
||||
@ -318,15 +316,17 @@
|
||||
<div class="card-title d-flex justify-content-between" :class="{ 'mb-0': !perk.description }">
|
||||
<span class="h5" :class="{ 'mb-0': !perk.description }">{{perk.title ? perk.title : perk.id}}</span>
|
||||
<span class="text-muted">
|
||||
<template v-if="perk.price && perk.price.value">
|
||||
{{formatAmount(perk.price.value.noExponents(), srvModel.currencyData.divisibility)}}
|
||||
{{targetCurrency}}
|
||||
<template v-if="perk.price.type == 1">or more</template>
|
||||
</template>
|
||||
<template v-else-if="perk.price.type === 2 && !perk.price.value">
|
||||
|
||||
<template v-if="perk.priceType === 'Fixed' && amount ==0">
|
||||
Free
|
||||
</template>
|
||||
<template v-else-if="perk.price.type === 0 || (!perk.price.value && perk.price.type === 1)">
|
||||
<template v-else-if="amount">
|
||||
{{formatAmount(perk.price.noExponents(), srvModel.currencyData.divisibility)}}
|
||||
{{targetCurrency}}
|
||||
<template v-if="perk.price.type === 'Minimum'">or more</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="perk.priceType === 'Topup' || (!amount && perk.priceType === 'Minimum')">
|
||||
Any amount
|
||||
</template>
|
||||
</span>
|
||||
@ -334,19 +334,18 @@
|
||||
<p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
|
||||
|
||||
<div class="input-group" style="max-width:500px;" v-if="expanded" :id="'perk-form'+ perk.id">
|
||||
<template v-if="perk.price.type !== 0 && !(perk.price.type === 2 && !perk.price.value)">
|
||||
<template v-if="perk.priceType !== 'Topup' && !(perk.priceType === 'Fixed' && amount == 0)">
|
||||
<input
|
||||
|
||||
:disabled="!active"
|
||||
:readonly="perk.price.type !== 1"
|
||||
:readonly="perk.priceType === 'Fixed'"
|
||||
class="form-control hide-number-spin"
|
||||
type="number"
|
||||
v-model="amount"
|
||||
:min="perk.price.value"
|
||||
:min="perk.price"
|
||||
step="any"
|
||||
placeholder="Contribution Amount"
|
||||
required>
|
||||
<span class="input-group-text" >{{targetCurrency}}</span>
|
||||
<span class="input-group-text">{{targetCurrency}}</span>
|
||||
</template>
|
||||
<button
|
||||
class="btn btn-primary d-flex align-items-center "
|
||||
@ -392,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>
|
||||
|
@ -165,19 +165,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
<partial name="TemplateEditor" model="@(nameof(Model.PerksTemplate), "Perks", Model.TargetCurrency ?? Model.StoreDefaultCurrency)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row collapse" id="RawEditor">
|
||||
<div class="col-xl-10 col-xxl-constrain">
|
||||
<div class="form-group pt-3">
|
||||
<label asp-for="PerksTemplate" class="form-label"></label>
|
||||
<textarea asp-for="PerksTemplate" rows="10" cols="40" class="form-control"></textarea>
|
||||
<span asp-validation-for="PerksTemplate" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="perks">
|
||||
<partial name="TemplateEditor" model="@(nameof(Model.PerksTemplate), Model.PerksTemplate, "Perks", Model.TargetCurrency ?? Model.StoreDefaultCurrency)" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
|
@ -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'
|
||||
|
@ -1,6 +1,8 @@
|
||||
@using BTCPayServer.Plugins.PointOfSale.Models
|
||||
@using BTCPayServer.Services
|
||||
@using Newtonsoft.Json.Linq;
|
||||
@model BTCPayServer.Plugins.PointOfSale.Models.ViewPointOfSaleViewModel
|
||||
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@{
|
||||
Layout = "PointOfSale/Public/_Layout";
|
||||
@ -20,6 +22,10 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.card:not(.d-none:only-of-type) {
|
||||
max-width: 320px;
|
||||
margin: auto !important;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
@section PageFootContent {
|
||||
@ -197,6 +203,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>
|
||||
@ -230,16 +238,30 @@
|
||||
{
|
||||
<div class="lead text-center mt-3">@Safe.Raw(Model.Description)</div>
|
||||
}
|
||||
@if (Model.AllCategories != null)
|
||||
{
|
||||
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-3 mt-3" data-toggle="buttons" v-pre>
|
||||
@foreach (var g in Model.AllCategories)
|
||||
{
|
||||
<input id="Category-@g.Value" type="radio" name="category" class="js-categories" value="@g.Value" @(g.Selected ? "checked" : "") autocomplete="off">
|
||||
<label class="btcpay-pill" for="Category-@g.Value">@g.Text</label>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div id="js-pos-list" class="text-center mx-auto px-2 px-sm-4 py-4 py-sm-2">
|
||||
<div class="card-deck">
|
||||
@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;
|
||||
|
||||
<div class="js-add-cart card px-0 card-wrapper" data-index="@index">
|
||||
<div class="js-add-cart card px-0 card-wrapper" data-index="@index" data-categories="@(new JArray(item.Categories).ToString())">
|
||||
@if (!string.IsNullOrWhiteSpace(image))
|
||||
{
|
||||
<img class="card-img-top" src="@image" alt="@Safe.Raw(item.Title)" asp-append-version="true">
|
||||
@ -252,10 +274,9 @@
|
||||
}
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0 pt-0 pb-3">
|
||||
|
||||
<span class="text-muted small">
|
||||
@{
|
||||
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
|
||||
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
|
||||
if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
@ -278,7 +299,8 @@
|
||||
<span>Sold out</span>
|
||||
}
|
||||
</div>
|
||||
} else if (anyInventoryItems)
|
||||
}
|
||||
else if (anyInventoryItems)
|
||||
{
|
||||
<div class="w-100 pt-2"> </div>
|
||||
}
|
||||
|
@ -7,6 +7,13 @@
|
||||
var anyInventoryItems = Model.Items.Any(item => item.Inventory.HasValue);
|
||||
}
|
||||
|
||||
<style>
|
||||
.card:only-of-type {
|
||||
max-width: 320px;
|
||||
margin: auto !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="container public-page-wrap flex-column">
|
||||
<partial name="_StatusMessage" />
|
||||
<partial name="_StoreHeader" model="(string.IsNullOrEmpty(Model.Title) ? Model.StoreName : Model.Title, Model.LogoFileId)" />
|
||||
|
@ -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>
|
||||
|
@ -50,10 +50,6 @@
|
||||
.card {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.card:only-of-type {
|
||||
max-width: 320px;
|
||||
margin: auto !important;
|
||||
}
|
||||
</style>
|
||||
@await RenderSectionAsync("PageHeadContent", false)
|
||||
</head>
|
||||
|
@ -76,20 +76,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div id="products">
|
||||
<div class="row">
|
||||
<div class="col-xxl-constrain">
|
||||
<partial name="TemplateEditor" model="@(nameof(Model.Template), "Products", Model.Currency ?? Model.StoreDefaultCurrency)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row collapse" id="RawEditor">
|
||||
<div class="col-xxl-constrain">
|
||||
<div class="form-group pt-3">
|
||||
<label asp-for="Template" class="form-label"></label>
|
||||
<textarea asp-for="Template" rows="10" cols="40" class="form-control"></textarea>
|
||||
<span asp-validation-for="Template" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<partial name="TemplateEditor" model="@(nameof(Model.Template), Model.Template, "Products", Model.Currency ?? Model.StoreDefaultCurrency)" />
|
||||
</div>
|
||||
<div class="row mt-5">
|
||||
<div class="col-sm-10 col-md-9 col-xl-7 col-xxl-6">
|
||||
@ -362,7 +349,6 @@
|
||||
el.removeAttribute('hidden');
|
||||
}
|
||||
function updateFormForDefaultView(type) {
|
||||
console.log(type)
|
||||
switch (type) {
|
||||
case 'Static':
|
||||
case 'Print':
|
||||
|
@ -1,4 +1,3 @@
|
||||
@using System.Text.RegularExpressions
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
|
@ -1,230 +1,278 @@
|
||||
@model (string templateId, string title, string currency)
|
||||
@model (string templateId, string template, string title, string currency)
|
||||
|
||||
<div id="template-editor-app" v-cloak>
|
||||
<div class="form-group mb-0">
|
||||
<h3 class="mt-5 mb-4">@Model.title</h3>
|
||||
@if (ViewContext.ViewData.ModelState.TryGetValue(Model.templateId, out var errors))
|
||||
{
|
||||
foreach (var error in errors.Errors)
|
||||
{
|
||||
<br/>
|
||||
<span class="text-danger" v-pre>@error.ErrorMessage</span>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="bg-light card">
|
||||
<div class="card-body " v-bind:class="{ 'card-deck': items.length > 0}">
|
||||
<div v-for="(item, index) of items" class="card my-2 card-wrapper template-item me-0 ms-0" v-bind:key="item.id">
|
||||
<div v-if="anyImages" class="card-img-top border-bottom" v-bind:style="getImage(item)"></div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title" v-html="item.title"></h6>
|
||||
<div class="gap-3 d-flex">
|
||||
<button type="button" class="btn btn-primary" v-on:click="editItem(index)">Edit</button>
|
||||
<button type="button" class="btn btn-danger" v-on:click="removeItem(index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!items || items.length === 0" class="col-12 text-center">
|
||||
No items.<br/>
|
||||
<button type="button" class="btn btn-link" v-on:click="editItem(-1)" id="btn-add-first">
|
||||
Add your first item
|
||||
<div class="modal" id="product-modal" tabindex="-1" role="dialog" ref="productModal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{editingItem && editingItem.id ? "Edit" : "Add"}} Item</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" ref="close">
|
||||
<vc:icon symbol="close" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-start p-3 gap-3 d-flex">
|
||||
<button type="button" class="btn btn-primary" v-on:click="editItem(-1)" id="btn-add">
|
||||
<i class="fa fa-plus fa-fw"></i> Add
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="ToggleRawEditor" data-bs-toggle="collapse" data-bs-target="#RawEditor" aria-expanded="false" aria-controls="RawEditor">
|
||||
Toggle raw editor
|
||||
</button>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<div class="text-danger mb-3" v-for="error of errors">{{error}}</div>
|
||||
<div class="form-group">
|
||||
<label for="EditorTitle" class="form-label" data-required>Title</label>
|
||||
<input id="EditorTitle" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem && editingItem.title" autofocus ref="txtTitle" />
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6">
|
||||
<label for="EditorPrice" class="form-label">Price</label>
|
||||
<select id="EditorPrice" class="form-select" v-model="editingItem && editingItem.priceType">
|
||||
<option v-for="option in customPriceOptions" :value="option.value">{{option.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6" v-show="editingItem && editingItem.priceType !== 'Topup'">
|
||||
<label for="EditorAmount" class="form-label"> </label>
|
||||
<div class="input-group mb-2">
|
||||
<input class="form-control"
|
||||
id="EditorAmount"
|
||||
inputmode="decimal"
|
||||
pattern="\d*"
|
||||
step="any"
|
||||
min="0"
|
||||
type="number"
|
||||
required
|
||||
v-model="editingItem && editingItem.price"
|
||||
ref="txtPrice"
|
||||
aria-describedby="currency-addon" />
|
||||
<span class="input-group-text" id="currency-addon" v-pre>@Model.currency</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="EditorImageUrl" class="form-label">Image Url</label>
|
||||
<input id="EditorImageUrl" class="form-control mb-2" pattern="[^\*#]+" v-model="editingItem && editingItem.image" ref="txtImage" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="EditorDescription" class="form-label">Description</label>
|
||||
<textarea id="EditorDescription" rows="3" cols="40" class="form-control mb-2" v-model="editingItem && editingItem.description" ref="txtDescription"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="EditorCategories" class="form-label">Categories</label>
|
||||
<input id="EditorCategories" class="form-control mb-2" autocomplete="off" ref="editorCategories" />
|
||||
<div class="form-text">Easily filter the different items using categories, used only in the product list with cart.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="EditorInventory" class="form-label">Inventory</label>
|
||||
<input id="EditorInventory" type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" v-model="editingItem && editingItem.inventory" ref="txtInventory" />
|
||||
<div class="form-text">Leave blank to not use this feature.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="EditorId" class="form-label">ID</label>
|
||||
<input id="EditorId" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem && editingItem.id" ref="txtId" />
|
||||
<div class="form-text">Leave blank to generate ID from title.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="BuyButtonText" class="form-label">Buy Button Text</label>
|
||||
<input id="BuyButtonText" type="text" class="form-control mb-2" v-model="editingItem && editingItem.buyButtonText" ref="txtBuyButtonText" />
|
||||
</div>
|
||||
<div class="form-group d-flex align-items-center">
|
||||
<input type="checkbox" id="Disabled" class="btcpay-toggle me-3" v-model="editingItem && editingItem.disabled" />
|
||||
<label for="Disabled" 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">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" v-on:click="saveItem()" id="SaveItemChanges">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" v-if="editingItem">{{editingItem.index>=0? "Edit" : "Add"}} Item</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" ref="close">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" v-if="editingItem">
|
||||
<div class="mb-3">
|
||||
<span class="text-danger row m-2" v-for="error of errors">{{error}}</span>
|
||||
<div class="form-group">
|
||||
<label class="form-label" data-required>Title</label>
|
||||
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.title" autofocus ref="txtTitle" />
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-6">
|
||||
<label class="form-label">Price</label>
|
||||
<select class="form-select" v-model="editingItem.priceType">
|
||||
<option v-for="option in customPriceOptions" :value="option.value">{{option.text}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xxl-constrain">
|
||||
<div class="form-group mb-0">
|
||||
<h3 class="mt-5 mb-4" v-pre>@Model.title</h3>
|
||||
@if (ViewContext.ViewData.ModelState.TryGetValue(Model.templateId, out var errors))
|
||||
{
|
||||
foreach (var error in errors.Errors)
|
||||
{
|
||||
<br />
|
||||
<span class="text-danger" v-pre>@error.ErrorMessage</span>
|
||||
}
|
||||
}
|
||||
<div class="bg-light card">
|
||||
<div class="card-body " v-bind:class="{ 'card-deck': config.length > 0}">
|
||||
<div v-if="!config || config.length === 0" class="col-12 text-center">
|
||||
No items.<br />
|
||||
<button type="button" class="btn btn-link" v-on:click="addItem()" id="btn-add-first">
|
||||
Add your first item
|
||||
</button>
|
||||
</div>
|
||||
<div v-else v-for="(item, index) of config" class="card my-2 card-wrapper template-item me-0 ms-0" v-bind:key="item.id">
|
||||
<div class="card-img-top border-bottom" v-bind:style="getImage(item)"></div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title" v-html="item.title"></h6>
|
||||
<div class="gap-3 d-flex">
|
||||
<button type="button" class="btn btn-primary" v-on:click="editItem(index)">Edit</button>
|
||||
<button type="button" class="btn btn-danger" v-on:click="removeItem(index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-6" v-show="editingItem.priceType !== 'Topup'">
|
||||
<label class="form-label"> </label>
|
||||
<div class="input-group mb-2">
|
||||
<input class="form-control"
|
||||
inputmode="decimal"
|
||||
pattern="\d*"
|
||||
step="any"
|
||||
min="0"
|
||||
type="number"
|
||||
required
|
||||
v-model="editingItem.price"
|
||||
ref="txtPrice"
|
||||
aria-describedby="currency-addon"/>
|
||||
<span class="input-group-text" id="currency-addon">@Model.currency</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Image Url</label>
|
||||
<input type="text" class="form-control mb-2" pattern="[^\*#]+" v-model="editingItem.image" ref="txtImage" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea rows="3" cols="40" class="form-control mb-2" v-model="editingItem.description" ref="txtDescription"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Inventory</label>
|
||||
<input type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" v-model="editingItem.inventory" ref="txtInventory" />
|
||||
<div class="form-text">Leave blank to not use this feature.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">ID</label>
|
||||
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.id" ref="txtId" />
|
||||
<div class="form-text">Leave blank to generate ID from title.</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Buy Button Text</label>
|
||||
<input type="text" id="BuyButtonText" class="form-control mb-2" v-model="editingItem.buyButtonText" ref="txtBuyButtonText" />
|
||||
</div>
|
||||
<div class="form-group d-flex align-items-center">
|
||||
<input type="checkbox" id="Disabled" class="btcpay-toggle me-3" v-model="editingItem.disabled" />
|
||||
<label class="form-label mb-0">Disabled</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" v-on:click="clearEditingItem()">Close</button>
|
||||
<button type="button" class="btn btn-primary" v-on:click="saveEditingItem()" id="SaveItemChanges">Save</button>
|
||||
<div class="card-footer text-start p-3 gap-3 d-flex">
|
||||
<button type="button" class="btn btn-primary" v-on:click="addItem()" id="btn-add">
|
||||
<i class="fa fa-plus fa-fw"></i> Add
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" id="ToggleRawEditor" data-bs-toggle="collapse" data-bs-target="#RawEditor" aria-expanded="false" aria-controls="RawEditor">
|
||||
Toggle raw editor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row collapse" id="RawEditor">
|
||||
<div class="col-xxl-constrain">
|
||||
<div class="form-group pt-3">
|
||||
<label for="@Model.templateId" class="form-label">Template</label>
|
||||
<textarea id="@Model.templateId" name="@Model.templateId" rows="10" cols="40" class="form-control" v-model="configJSON" v-on:change="updateFromJSON"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<link href="~/vendor/tom-select/tom-select.bootstrap5.min.css" asp-append-version="true" rel="stylesheet">
|
||||
<script src="~/vendor/tom-select/tom-select.complete.min.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const parseConfig = str => {
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (err) {
|
||||
console.error('Error deserializing template config:', err)
|
||||
}
|
||||
}
|
||||
const template = @Safe.Json(Model.template)
|
||||
let config = parseConfig(template) || []
|
||||
|
||||
new Vue({
|
||||
el: '#template-editor-app',
|
||||
data: {
|
||||
errors: [],
|
||||
items: [],
|
||||
editingItem: null,
|
||||
customPriceOptions: [
|
||||
{ text: 'Fixed', value: "Fixed" },
|
||||
{ text: 'Minimum', value: "Minimum" },
|
||||
{ text: 'Custom', value: 'Topup' },
|
||||
],
|
||||
elementId: "@Model.templateId"
|
||||
},
|
||||
computed: {
|
||||
anyImages: function(){
|
||||
return !!this.items.find(function(i){ return !!i.image;});
|
||||
data () {
|
||||
return {
|
||||
config,
|
||||
errors: [],
|
||||
editingIndex: null,
|
||||
editingItem: null,
|
||||
customPriceOptions: [
|
||||
{ text: 'Fixed', value: "Fixed" },
|
||||
{ text: 'Minimum', value: "Minimum" },
|
||||
{ text: 'Custom', value: 'Topup' },
|
||||
],
|
||||
categoriesSelect: null,
|
||||
productModal: null
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
this.load();
|
||||
this.getInputElement().on("input change", this.load.bind(this));
|
||||
this.getModalElement().on("hide.bs.modal", this.clearEditingItem.bind(this));
|
||||
mounted() {
|
||||
// modal
|
||||
const $modalEl = this.$refs.productModal;
|
||||
$modalEl.addEventListener('hide.bs.modal', () => { this.setEditingItem(null, null); });
|
||||
this.productModal = new bootstrap.Modal($modalEl, {})
|
||||
|
||||
// categories
|
||||
this.categoriesSelect = new TomSelect(this.$refs.editorCategories, {
|
||||
persist: false,
|
||||
createOnBlur: true,
|
||||
create: true,
|
||||
options: this.allCategories.map(value => ({ value, text: value })),
|
||||
});
|
||||
this.categoriesSelect.on('change', () => {
|
||||
const value = this.categoriesSelect.getValue();
|
||||
this.editingItem.categories = Array.from(value.split(',').reduce((res, item) => {
|
||||
const category = item.trim();
|
||||
if (category) res.add(category);
|
||||
return res;
|
||||
}, new Set()));
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.categoriesSelect.destroy();
|
||||
},
|
||||
computed: {
|
||||
allCategories() {
|
||||
return Array.from(this.config.reduce((res, item) => {
|
||||
(item.categories || []).forEach(category => { res.add(category); });
|
||||
return res;
|
||||
}, new Set()));
|
||||
},
|
||||
configJSON() {
|
||||
return JSON.stringify(this.config, null, 2)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getImage: function(item){
|
||||
var image = this.unEscapeKey(item.image) || "~/img/img-placeholder.svg";
|
||||
var url = image.startsWith("~") ? image.replace('~', window.location.pathname.substring(0, image.indexOf('/apps'))) : image;
|
||||
updateFromJSON(event) {
|
||||
const config = parseConfig(event.target.value)
|
||||
if (!config) return
|
||||
this.config = config
|
||||
},
|
||||
getImage(item) {
|
||||
const image = item.image || "~/img/img-placeholder.svg";
|
||||
const url = image.startsWith("~") ? image.replace('~', window.location.pathname.substring(0, image.indexOf('/apps'))) : image;
|
||||
return {
|
||||
"background-image" : "url('" + url +"')",
|
||||
"opacity": item.image? 1: 0.5
|
||||
}
|
||||
},
|
||||
getInputElement : function(){ return $("#" + this.elementId); },
|
||||
getModalElement : function(){ return $("#product-modal"); },
|
||||
load: function(){
|
||||
const template = this.getInputElement().val().trim();
|
||||
if (!template){
|
||||
this.items = [];
|
||||
} else {
|
||||
this.items = JSON.parse(template);
|
||||
}
|
||||
removeItem(index) {
|
||||
this.config.splice(index, 1);
|
||||
},
|
||||
save: function(){
|
||||
let template = JSON.stringify(this.items);
|
||||
this.getInputElement().val(template);
|
||||
addItem() {
|
||||
this.setEditingItem(null, { id: '', title: '', price: 0, image: '', description: '', categories: [], priceType: 'Fixed', inventory: null, disabled: false });
|
||||
},
|
||||
editItem: function(index){
|
||||
this.errors = [];
|
||||
if(index < 0){
|
||||
this.editingItem = {index:-1, id:"", title: "", price: 0, image: "", description: "", priceType: "Fixed", inventory: null, disabled: false};
|
||||
}else{
|
||||
this.editingItem = {...this.items[index], index};
|
||||
}
|
||||
|
||||
this.editingItem = this.unEscape(this.editingItem);
|
||||
this.getModalElement().modal("show");
|
||||
editItem(index) {
|
||||
this.setEditingItem(index, Object.assign({}, this.config[index]));
|
||||
},
|
||||
removeItem: function(index){
|
||||
this.items.splice(index,1);
|
||||
this.save();
|
||||
saveItem() {
|
||||
// set id from title if not set
|
||||
if (!this.editingItem.id) this.editingItem.id = this.editingItem.title.toLowerCase().trim();
|
||||
// validate
|
||||
if (!this.validate()) return;
|
||||
// add or update
|
||||
const idx = this.editingIndex === null ? this.config.length : this.editingIndex;
|
||||
this.$set(this.config, idx, this.editingItem);
|
||||
// update categories
|
||||
this.categoriesSelect.clearOptions();
|
||||
this.categoriesSelect.addOptions(this.allCategories.map(value => ({ value, text: value })));
|
||||
// hide modal
|
||||
this.productModal.hide();
|
||||
},
|
||||
clearEditingItem: function(){
|
||||
this.editingItem = null;
|
||||
this.errors = [];
|
||||
},
|
||||
validate: function(){
|
||||
validate () {
|
||||
this.errors = [];
|
||||
if (this.editingItem.id) {
|
||||
var matchedId = this.items.findIndex((x)=> { return this.unEscapeKey(x.id) === this.editingItem.id;});
|
||||
if( matchedId>= 0 && matchedId != this.editingItem.index)
|
||||
const matchedId = this.config.findIndex(x => x.id === this.editingItem.id);
|
||||
if (matchedId >= 0 && matchedId !== this.editingIndex)
|
||||
this.errors.push("You cannot have multiple items with the same id");
|
||||
|
||||
if (!this.$refs.txtId.checkValidity()) {
|
||||
this.errors.push("Id is required and cannot have * or #");
|
||||
}
|
||||
if(this.editingItem.id.startsWith("- ")){
|
||||
if (this.editingItem.id.startsWith("- "))
|
||||
this.errors.push("Id cannot start with \"- \"");
|
||||
}else if(this.editingItem.id.trim() == ""){
|
||||
else if (this.editingItem.id.trim() === "")
|
||||
this.errors.push("Id is required");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.editingItem.description.indexOf("*") >= 0 || this.editingItem.description.indexOf("#") >= 0) {
|
||||
this.errors.push("Description cannot have * or #");
|
||||
}
|
||||
if(this.editingItem.description.startsWith("- ")){
|
||||
if (this.editingItem.description.startsWith("- ")){
|
||||
this.errors.push("Description cannot start with \"- \"");
|
||||
}
|
||||
if (!this.$refs.txtImage.checkValidity()) {
|
||||
this.errors.push("Image cannot have * or #");
|
||||
}
|
||||
if(this.editingItem.image.startsWith("- ")){
|
||||
if (this.editingItem.image.startsWith("- ")){
|
||||
this.errors.push("Image cannot start with \"- \"");
|
||||
}
|
||||
|
||||
if (this.editingItem["priceType"] !== "Topup" && !this.$refs.txtPrice.checkValidity()) {
|
||||
this.errors.push("Price must be a valid number");
|
||||
}
|
||||
if (!this.$refs.txtTitle.checkValidity()) {
|
||||
this.errors.push("Title is required and cannot have * or #");
|
||||
}else if(this.editingItem.title.startsWith("- ")){
|
||||
} else if (this.editingItem.title.startsWith("- ")){
|
||||
this.errors.push("Title cannot start with \"- \"");
|
||||
}else if(this.editingItem.title.trim() == ""){
|
||||
} else if (!this.editingItem.title.trim()){
|
||||
this.errors.push("Title is required");
|
||||
}
|
||||
if (!this.$refs.txtInventory.checkValidity()) {
|
||||
@ -232,57 +280,22 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
return this.errors.length === 0;
|
||||
},
|
||||
saveEditingItem: function(){
|
||||
const fallbackId = this.editingItem.title.toLowerCase().trim();
|
||||
if(!this.editingItem.id && fallbackId){
|
||||
this.editingItem.id = fallbackId;
|
||||
this.$nextTick(this.saveEditingItem.bind(this));
|
||||
return;
|
||||
setEditingItem(index, item) {
|
||||
this.errors = [];
|
||||
this.editingIndex = index;
|
||||
this.editingItem = item;
|
||||
if (this.editingItem != null) {
|
||||
this.categoriesSelect.setValue(this.editingItem.categories);
|
||||
this.productModal.show();
|
||||
}
|
||||
if(!this.validate()){
|
||||
return;
|
||||
}
|
||||
this.editingItem = this.escape(this.editingItem);
|
||||
|
||||
if(this.editingItem.index < 0){
|
||||
this.items.push(this.editingItem);
|
||||
}else{
|
||||
this.items.splice(this.editingItem.index,1,this.editingItem);
|
||||
}
|
||||
this.save();
|
||||
this.getModalElement().modal("hide");
|
||||
},
|
||||
escape: function(item) {
|
||||
for(var k in item){
|
||||
if(k !== "paymentMethods" && k!=="id"){
|
||||
item[k] = $('<div/>').text(item[k]).html();
|
||||
}
|
||||
}
|
||||
return item;
|
||||
},
|
||||
unEscape: function(item){
|
||||
for(var k in item){
|
||||
if(k !== "paymentMethods" && k!=="id" && k !== "disabled"){
|
||||
item[k] = this.unEscapeKey(item[k]);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
},
|
||||
unEscapeKey : function(k){
|
||||
// Without this check a `false` boolean value will always be returned as an empty string
|
||||
if (k === false) {
|
||||
return "false";
|
||||
}
|
||||
|
||||
return $('<div/>').html(k).text();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Number.prototype.noExponents= function(){
|
||||
Number.prototype.noExponents = function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
if (data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
str= data[0].replace('.', ''),
|
||||
@ -297,5 +310,4 @@ Number.prototype.noExponents= function(){
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
@ -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>
|
||||
|
@ -43,7 +43,7 @@
|
||||
Write them down on a piece of paper in the exact order:
|
||||
</p>
|
||||
</div>
|
||||
<ol id="RecoveryPhrase" data-mnemonic="@Model.Mnemonic" class="my-5 mx-auto">
|
||||
<ol id="RecoveryPhrase" data-mnemonic="@Model.Mnemonic" class="d-inline-block my-5 mx-auto ps-4">
|
||||
@foreach (var word in Model.Words)
|
||||
{
|
||||
<li class="text-start text-muted py-2">
|
||||
|
@ -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>
|
||||
|
@ -12,6 +12,7 @@
|
||||
Layout = null;
|
||||
ViewData["Title"] = $"Receipt from {Model.StoreName}";
|
||||
var isProcessing = Model.Status == InvoiceStatus.Processing;
|
||||
var isFreeInvoice = (Model.Status == InvoiceStatus.New && Model.Amount == 0);
|
||||
var isSettled = Model.Status == InvoiceStatus.Settled;
|
||||
}
|
||||
<!DOCTYPE html>
|
||||
@ -22,8 +23,14 @@
|
||||
<meta name="robots" content="noindex,nofollow">
|
||||
@if (isProcessing)
|
||||
{
|
||||
<script type="text/javascript">
|
||||
setTimeout(() => { window.location.reload(); }, 10000);
|
||||
<script type="text/javascript">
|
||||
setTimeout(() => { window.location.reload(); }, 10000);
|
||||
</script>
|
||||
}
|
||||
else if (isFreeInvoice)
|
||||
{
|
||||
<script type="text/javascript">
|
||||
setTimeout(() => { window.location.reload(); }, 2000);
|
||||
</script>
|
||||
}
|
||||
<style>
|
||||
@ -64,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">
|
||||
|
@ -62,27 +62,36 @@
|
||||
</div>
|
||||
<hr class="border" />
|
||||
}
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="RateThenOption" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input"/>
|
||||
<label for="RateThenOption" class="form-check-label">@Model.RateThenText</label>
|
||||
<div class="form-text">The crypto currency price, at the rate the invoice got paid.</div>
|
||||
@if (Model.CryptoAmountThen > 0)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="RateThenOption" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input" />
|
||||
<label for="RateThenOption" class="form-check-label">@Model.RateThenText</label>
|
||||
<div class="form-text">The crypto currency price, at the rate the invoice got paid.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="CurrentRateOption" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input"/>
|
||||
<label for="CurrentRateOption" class="form-check-label">@Model.CurrentRateText</label>
|
||||
<div class="form-text">The crypto currency price, at the current rate.</div>
|
||||
}
|
||||
@if (Model.CryptoAmountNow > 0)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="CurrentRateOption" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input" />
|
||||
<label for="CurrentRateOption" class="form-check-label">@Model.CurrentRateText</label>
|
||||
<div class="form-text">The crypto currency price, at the current rate.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="FiatOption" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input"/>
|
||||
<label for="FiatOption" class="form-check-label">@Model.FiatText</label>
|
||||
<div class="form-text">The invoice currency, at the rate when the refund will be sent.</div>
|
||||
}
|
||||
@if (Model.FiatAmount > 0)
|
||||
{
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="FiatOption" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input" />
|
||||
<label for="FiatOption" class="form-check-label">@Model.FiatText</label>
|
||||
<div class="form-text">The invoice currency, at the rate when the refund will be sent.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input id="CustomOption" asp-for="SelectedRefundOption" type="radio" value="Custom" class="form-check-input"/>
|
||||
|
@ -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>.
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
.card-img-top {
|
||||
width: 100%;
|
||||
max-height: 180px;
|
||||
max-height: 210px;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ function Cart() {
|
||||
this.$summaryTip = $('.js-cart-summary-tip');
|
||||
this.$destroy = $('.js-cart-destroy');
|
||||
this.$confirm = $('#js-cart-confirm');
|
||||
|
||||
this.$categories = $('.js-categories');
|
||||
this.listItems();
|
||||
this.bindEmptyCart();
|
||||
|
||||
@ -112,7 +112,7 @@ Cart.prototype.getTotalProducts = function() {
|
||||
typeof this.content[key] != 'undefined' &&
|
||||
!this.content[key].disabled
|
||||
) {
|
||||
const price = this.toCents(this.content[key].price.value ||0);
|
||||
const price = this.toCents(this.content[key].price ||0);
|
||||
amount += (this.content[key].count * price);
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -419,7 +421,18 @@ Cart.prototype.listItems = function() {
|
||||
self = this,
|
||||
list = [],
|
||||
tableTemplate = '';
|
||||
|
||||
this.$categories.on('change', function (event) {
|
||||
if ($(this).is(':checked')) {
|
||||
var selectedCategory = $(this).val();
|
||||
$(".js-add-cart").each(function () {
|
||||
var categories = JSON.parse(this.getAttribute("data-categories"));
|
||||
if (selectedCategory === "*" || categories.includes(selectedCategory))
|
||||
this.classList.remove("d-none");
|
||||
else
|
||||
this.classList.add("d-none");
|
||||
});
|
||||
}
|
||||
});
|
||||
if (this.content.length > 0) {
|
||||
// Prepare the list of items in the cart
|
||||
for (var key in this.content) {
|
||||
@ -438,7 +451,7 @@ Cart.prototype.listItems = function() {
|
||||
'title': this.escape(item.title),
|
||||
'count': this.escape(item.count),
|
||||
'inventory': this.escape(item.inventory < 0? 99999: item.inventory),
|
||||
'price': this.escape(item.price.formatted || 0)
|
||||
'price': this.escape(item.price || 0)
|
||||
});
|
||||
list.push($(tableTemplate));
|
||||
}
|
||||
@ -690,7 +703,6 @@ Cart.prototype.destroy = function(keepAmount) {
|
||||
} else {
|
||||
this.removeItemAll();
|
||||
}
|
||||
|
||||
localStorage.removeItem(this.getStorageKey('cart'));
|
||||
}
|
||||
|
||||
|
@ -50,19 +50,22 @@ document.addEventListener("DOMContentLoaded",function (ev) {
|
||||
}
|
||||
},
|
||||
setAmount: function (amount) {
|
||||
this.amount = this.perk.price.type === 0? null : (amount || 0).noExponents();
|
||||
if(typeof amount === "string"){
|
||||
amount = parseFloat(amount);
|
||||
}
|
||||
this.amount = this.perk.priceType === "Topup"? null : (amount || 0).noExponents();
|
||||
this.expanded = false;
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
this.setAmount(this.perk.price.value);
|
||||
this.setAmount(this.perk.price);
|
||||
},
|
||||
watch: {
|
||||
perk: function (newValue, oldValue) {
|
||||
if(newValue.price.type ===0){
|
||||
if(newValue.price.type === "Topup"){
|
||||
this.setAmount();
|
||||
}else if (newValue.price.value != oldValue.price.value) {
|
||||
this.setAmount(newValue.price.value);
|
||||
}else if (newValue.price != oldValue.price) {
|
||||
this.setAmount(newValue.price);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -147,9 +150,9 @@ document.addEventListener("DOMContentLoaded",function (ev) {
|
||||
return result;
|
||||
},
|
||||
perks: function(){
|
||||
var result = [];
|
||||
for (var i = 0; i < this.srvModel.perks.length; i++) {
|
||||
var currentPerk = this.srvModel.perks[i];
|
||||
const result = [];
|
||||
for (let i = 0; i < this.srvModel.perks.length; i++) {
|
||||
const currentPerk = this.srvModel.perks[i];
|
||||
if(this.srvModel.perkCount.hasOwnProperty(currentPerk.id)){
|
||||
currentPerk.sold = this.srvModel.perkCount[currentPerk.id];
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
Number.prototype.noExponents= function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
String.prototype.noExponents= function(){
|
||||
const data = String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
@ -14,4 +14,8 @@ Number.prototype.noExponents= function(){
|
||||
mag -= str.length;
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
}
|
||||
|
||||
Number.prototype.noExponents= function(){
|
||||
return String(this).noExponents();
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -10,18 +10,17 @@
|
||||
|
||||
/* Hide sensitive info */
|
||||
[data-hide-sensitive-info="true"] [data-sensitive] {
|
||||
visibility: hidden;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-hide-sensitive-info="true"] [data-sensitive]:before {
|
||||
content: '***';
|
||||
content: '***********************';
|
||||
visibility: visible;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
[data-hide-sensitive-info="true"] .text-end [data-sensitive]:before {
|
||||
right: 0;
|
||||
top: .2em;
|
||||
}
|
||||
|
||||
[data-hide-sensitive-info="true"] .store-wallet-balance .ct-label.ct-vertical.ct-start {
|
||||
@ -158,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>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user