Compare commits
45 Commits
v1.13.0-rc
...
feat/lnd-0
Author | SHA1 | Date | |
---|---|---|---|
abf4fc9b5d | |||
c7231fe092 | |||
8922c3de59 | |||
fefb99dfa2 | |||
a3b0bbe861 | |||
3dd562ffdc | |||
b19db7291d | |||
70253cbd9f | |||
887803a328 | |||
42da90f7dc | |||
1152f68aed | |||
9124aeb1ee | |||
a35c5d8289 | |||
e24b42ef95 | |||
e10937c253 | |||
96b90d2444 | |||
600bbb9ce0 | |||
fe9e5eb9c9 | |||
cb136cba82 | |||
b3240f28b5 | |||
fe32cbd8be | |||
51fcf52da1 | |||
3f02c0d30a | |||
bae1f4e20b | |||
3fbc717cd4 | |||
958a348fed | |||
57226fc97f | |||
ca55e1f300 | |||
8b02c0bd82 | |||
b92ff7c27b | |||
d24761a498 | |||
c78ee24d0a | |||
172dd507bd | |||
fdd4790023 | |||
4ebe46830b | |||
a2df9ed44c | |||
6ae474d214 | |||
5b31d4de20 | |||
14f8c73b08 | |||
529075f64c | |||
dba102e74f | |||
0f3f8b6bf9 | |||
83028b9b73 | |||
1fe766cb16 | |||
6b45eb0d3d |
BTCPayServer.Abstractions
BTCPayServer.Client
BTCPayServer.Common
BTCPayServer.Data
BTCPayServer.Rating
BTCPayServer.Tests
FastTests.csSeleniumTests.csThirdPartyTests.csUnitTest1.csdocker-compose.altcoins.ymldocker-compose.yml
BTCPayServer
BTCPayServer.csproj
Components
MainNav
StoreRecentInvoices
StoreRecentTransactions
Controllers
GreenField
UIInvoiceController.UI.csUILNURLController.csUIPaymentRequestController.csUIPullPaymentController.csUIReportsController.CheatMode.csUIServerController.Roles.csUIStorePullPaymentsController.PullPayments.csUIStoresController.Users.csUIWalletsController.PSBT.csUIWalletsController.csData/Payouts/LightningLike
Filters
HostedServices
Hosting
Models
InvoicingModels
WalletViewModels
Payments/Lightning
Plugins/Altcoins/Monero/RPC/Models
SearchString.csServices
Altcoins/Monero
Reporting
Wallets/Export
Views
Shared
Crowdfund
EmailsBody.cshtmlPointOfSale
PosData.cshtmlTemplateEditor.cshtml_BTCPaySupporters.cshtml_LayoutSignedOut.cshtml_LayoutSimple.cshtmlUIInvoice
UIPaymentRequest
UIPullPayment
UIReports
UIServer
UIStorePullPayments
UIStores
wwwroot
Build
Changelog.mdDockerfileREADME.mddocs
@ -32,8 +32,8 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlSanitizer" Version="8.0.838" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.5" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0-beta.2" />
|
||||
</ItemGroup>
|
||||
|
@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Npgsql;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Migrations.Operations;
|
||||
|
||||
@ -85,10 +86,9 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
{
|
||||
o.EnableRetryOnFailure(10);
|
||||
o.SetPostgresVersion(12, 0);
|
||||
if (!string.IsNullOrEmpty(_schemaPrefix))
|
||||
{
|
||||
o.MigrationsHistoryTable(_schemaPrefix);
|
||||
}
|
||||
var mainSearchPath = GetSearchPath(_options.Value.ConnectionString);
|
||||
var schemaPrefix = string.IsNullOrEmpty(_schemaPrefix) ? "__EFMigrationsHistory" : _schemaPrefix;
|
||||
o.MigrationsHistoryTable(schemaPrefix, mainSearchPath);
|
||||
})
|
||||
.ReplaceService<IMigrationsSqlGenerator, CustomNpgsqlMigrationsSqlGenerator>();
|
||||
break;
|
||||
@ -108,5 +108,11 @@ namespace BTCPayServer.Abstractions.Contracts
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSearchPath(string connectionString)
|
||||
{
|
||||
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString);
|
||||
var searchPaths = connectionStringBuilder.SearchPath?.Split(',');
|
||||
return searchPaths is not { Length: > 0 } ? null : searchPaths[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,17 @@ public static class HttpRequestExtensions
|
||||
request.Path.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentUrlWithQueryString(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
request.Scheme,
|
||||
"://",
|
||||
request.Host.ToUriComponent(),
|
||||
request.PathBase.ToUriComponent(),
|
||||
request.Path.ToUriComponent(),
|
||||
request.QueryString.ToUriComponent());
|
||||
}
|
||||
|
||||
public static string GetCurrentPath(this HttpRequest request)
|
||||
{
|
||||
return string.Concat(
|
||||
|
@ -31,7 +31,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.5.1" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.34" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.37" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="4.3.1" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="'$(Altcoins)' != 'true'">
|
||||
|
@ -3,11 +3,11 @@
|
||||
<Import Project="../Build/Common.csproj" />
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\BTCPayServer.Abstractions\BTCPayServer.Abstractions.csproj" />
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.34" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.37" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
@ -1347,17 +1347,22 @@ bc1qfzu57kgu5jthl934f9xrdzzx8mmemx7gn07tf0grnvz504j6kzusu2v0ku
|
||||
|
||||
filter = "status:abed, status:abed2";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("", search.TextSearch);
|
||||
Assert.Null(search.TextSearch);
|
||||
Assert.Null(search.TextFilters);
|
||||
Assert.Equal("status:abed, status:abed2", search.ToString());
|
||||
Assert.Throws<KeyNotFoundException>(() => search.Filters["test"]);
|
||||
Assert.Equal(2, search.Filters["status"].Count);
|
||||
Assert.Equal("abed", search.Filters["status"].First());
|
||||
Assert.Equal("abed2", search.Filters["status"].Skip(1).First());
|
||||
|
||||
filter = "StartDate:2019-04-25 01:00 AM, hekki";
|
||||
filter = "StartDate:2019-04-25 01:00 AM, hekki,orderid:MYORDERID,orderid:MYORDERID_2";
|
||||
search = new SearchString(filter);
|
||||
Assert.Equal("2019-04-25 01:00 AM", search.Filters["startdate"].First());
|
||||
Assert.Equal("hekki", search.TextSearch);
|
||||
Assert.Equal("orderid:MYORDERID,orderid:MYORDERID_2", search.TextFilters);
|
||||
Assert.Equal("orderid:MYORDERID,orderid:MYORDERID_2,hekki", search.TextCombined);
|
||||
Assert.Equal("StartDate:2019-04-25 01:00 AM", search.WithoutSearchText());
|
||||
Assert.Equal(filter, search.ToString());
|
||||
|
||||
// modify search
|
||||
filter = $"status:settled,exceptionstatus:paidLate,unusual:true, fulltext searchterm, storeid:{storeId},startdate:2019-04-25 01:00:00";
|
||||
|
@ -2620,9 +2620,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
var items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
var cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||
var items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(2, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Manual entry 1", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
@ -2676,21 +2676,19 @@ namespace BTCPayServer.Tests
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
|
||||
additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||
items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||
sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(3, items.Count);
|
||||
Assert.Equal(2, sums.Count);
|
||||
Assert.Single(sums);
|
||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1 x 1,00 € = 1,00 €", items[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Green Tea", items[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("2 x 1,00 € = 2,00 €", items[1].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Manual entry 1", items[2].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("1,23 €", items[2].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Subtotal", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("Total", sums[0].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[0].FindElement(By.CssSelector("td")).Text);
|
||||
Assert.Contains("Total", sums[1].FindElement(By.CssSelector("th")).Text);
|
||||
Assert.Contains("4,23 €", sums[1].FindElement(By.CssSelector("td")).Text);
|
||||
|
||||
// Guest user can access recent transactions
|
||||
s.GoToHome();
|
||||
@ -2837,9 +2835,9 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// Receipt
|
||||
s.Driver.WaitForElement(By.Id("ReceiptLink")).Click();
|
||||
var additionalData = s.Driver.FindElement(By.CssSelector("#AdditionalData table"));
|
||||
var items = additionalData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = additionalData.FindElements(By.CssSelector("tfoot tr"));
|
||||
var cartData = s.Driver.FindElement(By.CssSelector("#CartData table"));
|
||||
var items = cartData.FindElements(By.CssSelector("tbody tr"));
|
||||
var sums = cartData.FindElements(By.CssSelector("tfoot tr"));
|
||||
Assert.Equal(7, items.Count);
|
||||
Assert.Equal(4, sums.Count);
|
||||
Assert.Contains("Black Tea", items[0].FindElement(By.CssSelector("th")).Text);
|
||||
|
@ -464,7 +464,7 @@ retry:
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
version = Regex.Match(actual, "Original file: /npm/dom-confetti@([0-9]+.[0-9]+.[0-9]+)/lib/main.js").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}/lib/main.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/dom-confetti@{version}")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "vue-sortable", "sortable.min.js").Trim();
|
||||
|
@ -2985,7 +2985,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateReports()
|
||||
{
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
@ -3093,6 +3093,48 @@ namespace BTCPayServer.Tests
|
||||
var invoiceIdIndex = report.GetIndex("InvoiceId");
|
||||
var oldPaymentsCount = report.Data.Count(d => d[invoiceIdIndex].Value<string>() == "Q7RqoHLngK9svM4MgRyi9y");
|
||||
Assert.Equal(8, oldPaymentsCount); // 10 payments, but 2 unaccounted
|
||||
|
||||
var addr = await tester.ExplorerNode.GetNewAddressAsync();
|
||||
// Two invoices get refunded
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
var inv = await client.CreateInvoice(acc.StoreId, new CreateInvoiceRequest() { Amount = 10m, Currency = "USD" });
|
||||
await acc.PayInvoice(inv.Id);
|
||||
await client.MarkInvoiceStatus(acc.StoreId, inv.Id, new MarkInvoiceStatusRequest() { Status = InvoiceStatus.Settled });
|
||||
var refund = await client.RefundInvoice(acc.StoreId, inv.Id, new RefundInvoiceRequest() { RefundVariant = RefundVariant.Fiat, PaymentMethod = "BTC" });
|
||||
|
||||
async Task AssertData(string currency, decimal awaiting, decimal limit, decimal completed, bool fullyPaid)
|
||||
{
|
||||
report = await GetReport(acc, new() { ViewName = "Refunds" });
|
||||
var currencyIndex = report.GetIndex("Currency");
|
||||
var awaitingIndex = report.GetIndex("Awaiting");
|
||||
var fullyPaidIndex = report.GetIndex("FullyPaid");
|
||||
var completedIndex = report.GetIndex("Completed");
|
||||
var limitIndex = report.GetIndex("Limit");
|
||||
var d = Assert.Single(report.Data.Where(d => d[report.GetIndex("InvoiceId")].Value<string>() == inv.Id));
|
||||
Assert.Equal(fullyPaid, (bool)d[fullyPaidIndex]);
|
||||
Assert.Equal(currency, d[currencyIndex].Value<string>());
|
||||
Assert.Equal(completed, (((JObject)d[completedIndex])["v"]).Value<decimal>());
|
||||
Assert.Equal(awaiting, (((JObject)d[awaitingIndex])["v"]).Value<decimal>());
|
||||
Assert.Equal(limit, (((JObject)d[limitIndex])["v"]).Value<decimal>());
|
||||
}
|
||||
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
var payout = await client.CreatePayout(refund.Id, new CreatePayoutRequest() { Destination = addr.ToString(), PaymentMethod = "BTC" });
|
||||
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
await client.ApprovePayout(acc.StoreId, payout.Id, new ApprovePayoutRequest());
|
||||
await AssertData("USD", awaiting: 10.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
if (i == 0)
|
||||
{
|
||||
await client.MarkPayoutPaid(acc.StoreId, payout.Id);
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 10.0m, fullyPaid: true);
|
||||
}
|
||||
if (i == 1)
|
||||
{
|
||||
await client.CancelPayout(acc.StoreId, payout.Id);
|
||||
await AssertData("USD", awaiting: 0.0m, limit: 10.0m, completed: 0.0m, fullyPaid: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<StoreReportResponse> GetReport(TestAccount acc, StoreReportRequest req)
|
||||
|
@ -99,7 +99,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.0
|
||||
image: nicolasdorier/nbxplorer:2.5.4
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -163,7 +163,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -171,6 +171,7 @@ services:
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd:9735
|
||||
@ -190,13 +191,14 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd:9735
|
||||
@ -224,7 +226,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
image: btcpayserver/lnd:v0.18.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -259,7 +261,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
image: btcpayserver/lnd:v0.18.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -96,7 +96,7 @@ services:
|
||||
custom:
|
||||
|
||||
nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:2.5.0
|
||||
image: nicolasdorier/nbxplorer:2.5.4
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "32838:32838"
|
||||
@ -149,7 +149,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -157,6 +157,7 @@ services:
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=customer_lightningd:9735
|
||||
@ -176,13 +177,14 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
image: btcpayserver/lightning:v24.02.2
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
LIGHTNINGD_CHAIN: "btc"
|
||||
LIGHTNINGD_NETWORK: "regtest"
|
||||
LIGHTNINGD_OPT: |
|
||||
developer
|
||||
bitcoin-datadir=/etc/bitcoin
|
||||
bitcoin-rpcconnect=bitcoind
|
||||
announce-addr=merchant_lightningd:9735
|
||||
@ -211,7 +213,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
image: btcpayserver/lnd:v0.18.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -248,7 +250,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.17.4-beta
|
||||
image: btcpayserver/lnd:v0.18.0-beta
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -46,13 +46,13 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.22" />
|
||||
<PackageReference Include="BTCPayServer.NTag424" Version="1.0.23" />
|
||||
<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.5.4" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.1.28" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.0" />
|
||||
<PackageReference Include="CsvHelper" Version="32.0.3" />
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="LNURL" Version="0.0.34" />
|
||||
@ -77,8 +77,8 @@
|
||||
<PackageReference Include="TwentyTwenty.Storage.Azure" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Google" Version="2.12.1" />
|
||||
<PackageReference Include="TwentyTwenty.Storage.Local" Version="2.12.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -296,6 +296,15 @@
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ContactUrl))
|
||||
{
|
||||
<li class="nav-item">
|
||||
<a href="@Model.ContactUrl" class="nav-link" id="Nav-ContactUs">
|
||||
<vc:icon symbol="contact"/>
|
||||
<span>Contact Us</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</nav>
|
||||
|
@ -29,6 +29,8 @@ namespace BTCPayServer.Components.MainNav
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly PaymentMethodHandlerDictionary _paymentMethodHandlerDictionary;
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
|
||||
public MainNav(
|
||||
AppService appService,
|
||||
@ -38,6 +40,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
UserManager<ApplicationUser> userManager,
|
||||
PaymentMethodHandlerDictionary paymentMethodHandlerDictionary,
|
||||
CustodianAccountRepository custodianAccountRepository,
|
||||
SettingsRepository settingsRepository,
|
||||
PoliciesSettings policiesSettings)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
@ -47,13 +50,19 @@ namespace BTCPayServer.Components.MainNav
|
||||
_storesController = storesController;
|
||||
_paymentMethodHandlerDictionary = paymentMethodHandlerDictionary;
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
_settingsRepository = settingsRepository;
|
||||
PoliciesSettings = policiesSettings;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync()
|
||||
{
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
var vm = new MainNavViewModel { Store = store };
|
||||
var serverSettings = await _settingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
|
||||
var vm = new MainNavViewModel
|
||||
{
|
||||
Store = store,
|
||||
ContactUrl = serverSettings.ContactUrl
|
||||
};
|
||||
#if ALTCOINS
|
||||
vm.AltcoinsBuild = true;
|
||||
#endif
|
||||
@ -92,7 +101,5 @@ namespace BTCPayServer.Components.MainNav
|
||||
}
|
||||
|
||||
private string UserId => _userManager.GetUserId(HttpContext.User);
|
||||
|
||||
public PoliciesSettings PoliciesSettings { get; }
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
public CustodianAccountData[] CustodianAccounts { get; set; }
|
||||
public bool AltcoinsBuild { get; set; }
|
||||
public int ArchivedAppsCount { get; set; }
|
||||
public string ContactUrl { get; set; }
|
||||
}
|
||||
|
||||
public class StoreApp
|
||||
|
@ -34,34 +34,36 @@
|
||||
}
|
||||
else if (Model.Invoices.Any())
|
||||
{
|
||||
<table class="table table-hover mt-3 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<div class="table-responsive mt-3 mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
|
||||
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
</td>
|
||||
<th class="w-125px">Date</th>
|
||||
<th class="text-nowrap">Invoice Id</th>
|
||||
<th>Status</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var invoice in Model.Invoices)
|
||||
{
|
||||
<tr>
|
||||
<td>@invoice.Date.ToTimeAgo()</td>
|
||||
<td>
|
||||
<a asp-controller="UIInvoice" asp-action="Invoice" asp-route-invoiceId="@invoice.InvoiceId" class="text-break">@invoice.InvoiceId</a>
|
||||
</td>
|
||||
<td>
|
||||
<vc:invoice-status state="invoice.Status" payments="invoice.Details.Payments" invoice-id="@invoice.InvoiceId"
|
||||
is-archived="invoice.Details.Archived" has-refund="invoice.HasRefund" />
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(invoice.Amount, invoice.Currency)</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -31,61 +31,63 @@
|
||||
}
|
||||
else if (Model.Transactions.Any())
|
||||
{
|
||||
<table class="table table-hover mt-3 mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<div class="table-responsive mt-3 mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td>
|
||||
@if (tx.Labels.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@foreach (var label in tx.Labels)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||
<span>@label.Text</span>
|
||||
@if (!string.IsNullOrEmpty(label.Link))
|
||||
{
|
||||
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
||||
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
|
||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
<th class="w-125px">Date</th>
|
||||
<th>Transaction</th>
|
||||
<th>Labels</th>
|
||||
<th class="text-end">Amount</th>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var tx in Model.Transactions)
|
||||
{
|
||||
<tr>
|
||||
<td>@tx.Timestamp.ToTimeAgo()</td>
|
||||
<td>
|
||||
<vc:truncate-center text="@tx.Id" link="@tx.Link" classes="truncate-center-id" />
|
||||
</td>
|
||||
<td>
|
||||
@if (tx.Labels.Any())
|
||||
{
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center">
|
||||
@foreach (var label in tx.Labels)
|
||||
{
|
||||
<div class="transaction-label" style="--label-bg:@label.Color;--label-fg:@label.TextColor">
|
||||
<span>@label.Text</span>
|
||||
@if (!string.IsNullOrEmpty(label.Link))
|
||||
{
|
||||
<a class="transaction-label-info transaction-details-icon" href="@label.Link"
|
||||
target="_blank" rel="noreferrer noopener" title="@label.Tooltip" data-bs-html="true"
|
||||
data-bs-toggle="tooltip" data-bs-custom-class="transaction-label-tooltip">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</td>
|
||||
@if (tx.Positive)
|
||||
{
|
||||
<td class="text-end text-success">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
else
|
||||
{
|
||||
<td class="text-end text-danger">
|
||||
<span data-sensitive>@DisplayFormatter.Currency(tx.Balance, tx.Currency)</span>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -14,7 +14,6 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.NTag424;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Security;
|
||||
@ -29,7 +28,6 @@ using Microsoft.Extensions.Logging;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Org.BouncyCastle.Bcpg.OpenPgp;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
@ -49,7 +47,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly Logs _logs;
|
||||
|
||||
public GreenfieldPullPaymentController(PullPaymentHostedService pullPaymentService,
|
||||
LinkGenerator linkGenerator,
|
||||
@ -60,7 +57,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
IAuthorizationService authorizationService,
|
||||
SettingsRepository settingsRepository,
|
||||
BTCPayServerEnvironment env, Logs logs)
|
||||
BTCPayServerEnvironment env)
|
||||
{
|
||||
_pullPaymentService = pullPaymentService;
|
||||
_linkGenerator = linkGenerator;
|
||||
@ -72,7 +69,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_authorizationService = authorizationService;
|
||||
_settingsRepository = settingsRepository;
|
||||
_env = env;
|
||||
_logs = logs;
|
||||
}
|
||||
|
||||
[HttpGet("~/api/v1/stores/{storeId}/pull-payments")]
|
||||
@ -161,20 +157,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return this.CreateValidationError(ModelState);
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(new CreatePullPayment()
|
||||
{
|
||||
StartsAt = request.StartsAt,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
Period = request.Period,
|
||||
BOLT11Expiration = request.BOLT11Expiration,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PaymentMethodIds = paymentMethods,
|
||||
AutoApproveClaims = request.AutoApproveClaims
|
||||
});
|
||||
var ppId = await _pullPaymentService.CreatePullPayment(storeId, request);
|
||||
var pp = await _pullPaymentService.GetPullPayment(ppId, false);
|
||||
return this.Ok(CreatePullPaymentData(pp));
|
||||
}
|
||||
@ -212,8 +195,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
if (pullPaymentId is null)
|
||||
return PullPaymentNotFound();
|
||||
this._logs.PayServer.LogInformation($"RegisterBoltcard: onExisting queryParam: {onExisting}");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
||||
var pp = await _pullPaymentService.GetPullPayment(pullPaymentId, false);
|
||||
if (pp is null)
|
||||
return PullPaymentNotFound();
|
||||
@ -259,13 +240,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_ => request.OnExisting
|
||||
};
|
||||
|
||||
this._logs.PayServer.LogInformation($"After");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(request)}");
|
||||
|
||||
var version = await _dbContextFactory.LinkBoltcardToPullPayment(pullPaymentId, issuerKey, request.UID, request.OnExisting);
|
||||
this._logs.PayServer.LogInformation($"Version: " + version);
|
||||
this._logs.PayServer.LogInformation($"ID: " + Encoders.Hex.EncodeData(issuerKey.GetId(request.UID)));
|
||||
|
||||
var keys = issuerKey.CreatePullPaymentCardKey(request.UID, version, pullPaymentId).DeriveBoltcardKeys(issuerKey);
|
||||
|
||||
var boltcardUrl = Url.Action(nameof(UIBoltcardController.GetWithdrawRequest), "UIBoltcard");
|
||||
@ -282,9 +257,6 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
K3 = Encoders.Hex.EncodeData(keys.K3.ToBytes()).ToUpperInvariant(),
|
||||
K4 = Encoders.Hex.EncodeData(keys.K4.ToBytes()).ToUpperInvariant(),
|
||||
};
|
||||
this._logs.PayServer.LogInformation($"Response");
|
||||
this._logs.PayServer.LogInformation($"{JsonConvert.SerializeObject(resp)}");
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
|
@ -162,10 +162,10 @@ namespace BTCPayServer.Controllers
|
||||
model.Overpaid = details.Overpaid;
|
||||
model.StillDue = details.StillDue;
|
||||
model.HasRates = details.HasRates;
|
||||
|
||||
if (additionalData.ContainsKey("receiptData"))
|
||||
|
||||
if (additionalData.TryGetValue("receiptData", out object? receiptData))
|
||||
{
|
||||
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
|
||||
model.ReceiptData = (Dictionary<string, object>)receiptData;
|
||||
additionalData.Remove("receiptData");
|
||||
}
|
||||
|
||||
@ -226,15 +226,40 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
JToken? receiptData = null;
|
||||
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
|
||||
|
||||
var metaData = PosDataParser.ParsePosData(i.Metadata?.ToJObject());
|
||||
var additionalData = metaData
|
||||
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
|
||||
.ToDictionary(dict => dict.Key, dict => dict.Value);
|
||||
|
||||
// Split receipt data into cart and additional data
|
||||
if (additionalData.TryGetValue("receiptData", out object? combinedReceiptData))
|
||||
{
|
||||
var receiptData = new Dictionary<string, object>((Dictionary<string, object>)combinedReceiptData, StringComparer.OrdinalIgnoreCase);
|
||||
string[] cartKeys = ["cart", "subtotal", "discount", "tip", "total"];
|
||||
// extract cart data and lowercase keys to handle data uniformly in PosData partial
|
||||
if (receiptData.Keys.Any(key => cartKeys.Contains(key.ToLowerInvariant())))
|
||||
{
|
||||
vm.CartData = new Dictionary<string, object>();
|
||||
foreach (var key in cartKeys)
|
||||
{
|
||||
if (!receiptData.ContainsKey(key)) continue;
|
||||
// add it to cart data and remove it from the general data
|
||||
vm.CartData.Add(key.ToLowerInvariant(), receiptData[key]);
|
||||
receiptData.Remove(key);
|
||||
}
|
||||
}
|
||||
// assign the rest to additional data
|
||||
if (receiptData.Any())
|
||||
{
|
||||
vm.AdditionalData = receiptData;
|
||||
}
|
||||
}
|
||||
|
||||
var payments = ViewPaymentRequestViewModel.PaymentRequestInvoicePayment.GetViewModels(i, _displayFormatter, _transactionLinkProviders);
|
||||
|
||||
vm.Amount = i.PaidAmount.Net;
|
||||
vm.Payments = receipt.ShowPayments is false ? null : payments;
|
||||
vm.AdditionalData = PosDataParser.ParsePosData(receiptData);
|
||||
|
||||
return View(print ? "InvoiceReceiptPrint" : "InvoiceReceipt", vm);
|
||||
}
|
||||
@ -1073,7 +1098,7 @@ namespace BTCPayServer.Controllers
|
||||
storeIds.Add(i);
|
||||
}
|
||||
model.Search = fs;
|
||||
model.SearchText = fs.TextSearch;
|
||||
model.SearchText = fs.TextCombined;
|
||||
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, apps, timezoneOffset);
|
||||
|
@ -296,11 +296,11 @@ namespace BTCPayServer
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var createInvoice = new CreateInvoiceRequest()
|
||||
var createInvoice = new CreateInvoiceRequest
|
||||
{
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup? null: item?.Price,
|
||||
Amount = item?.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? null : item?.Price,
|
||||
Currency = currencyCode,
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions()
|
||||
Checkout = new InvoiceDataBase.CheckoutOptions
|
||||
{
|
||||
RedirectURL = app.AppType switch
|
||||
{
|
||||
@ -312,6 +312,7 @@ namespace BTCPayServer
|
||||
AdditionalSearchTerms = new[] { AppService.GetAppSearchTerm(app) }
|
||||
};
|
||||
|
||||
var allowOverpay = item?.PriceType is not ViewPointOfSaleViewModel.ItemPriceType.Fixed;
|
||||
var invoiceMetadata = new InvoiceMetadata { OrderId = AppService.GetRandomOrderId() };
|
||||
if (item != null)
|
||||
{
|
||||
@ -326,7 +327,7 @@ namespace BTCPayServer
|
||||
store.GetStoreBlob(),
|
||||
createInvoice,
|
||||
additionalTags: new List<string> { AppService.GetAppInternalTag(appId) },
|
||||
allowOverpay: false);
|
||||
allowOverpay: allowOverpay);
|
||||
}
|
||||
|
||||
public class EditLightningAddressVM
|
||||
@ -495,7 +496,7 @@ namespace BTCPayServer
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IActionResult> GetLNURLRequest(
|
||||
public async Task<IActionResult> GetLNURLRequest(
|
||||
string cryptoCode,
|
||||
Data.StoreData store,
|
||||
Data.StoreBlob blob,
|
||||
@ -522,7 +523,9 @@ namespace BTCPayServer
|
||||
return this.CreateAPIError(null, e.Message);
|
||||
}
|
||||
lnurlRequest = await CreateLNUrlRequestFromInvoice(cryptoCode, i, store, blob, lnurlRequest, lnUrlMetadata, allowOverpay);
|
||||
return lnurlRequest is null ? NotFound() : Ok(lnurlRequest);
|
||||
return lnurlRequest is null
|
||||
? BadRequest(new LNUrlStatusResponse { Status = "ERROR", Reason = "Unable to create LNURL request." })
|
||||
: Ok(lnurlRequest);
|
||||
}
|
||||
|
||||
private async Task<LNURLPayRequest> CreateLNUrlRequestFromInvoice(
|
||||
|
@ -277,6 +277,10 @@ namespace BTCPayServer.Controllers
|
||||
if (FormDataService.Validate(form, ModelState))
|
||||
{
|
||||
prBlob.FormResponse = FormDataService.GetValues(form);
|
||||
if(string.IsNullOrEmpty(prBlob.Email) && form.GetFieldByFullName("buyerEmail") is { } emailField)
|
||||
{
|
||||
prBlob.Email = emailField.Value;
|
||||
}
|
||||
result.SetBlob(prBlob);
|
||||
await _PaymentRequestRepository.CreateOrUpdatePaymentRequest(result);
|
||||
return RedirectToAction("PayPaymentRequest", new { payReqId });
|
||||
|
@ -227,6 +227,7 @@ namespace BTCPayServer.Controllers
|
||||
var supported = ppBlob.SupportedPaymentMethods;
|
||||
PaymentMethodId paymentMethodId = null;
|
||||
IClaimDestination destination = null;
|
||||
string error = null;
|
||||
if (string.IsNullOrEmpty(vm.SelectedPaymentMethod))
|
||||
{
|
||||
foreach (var pmId in supported)
|
||||
@ -235,6 +236,7 @@ namespace BTCPayServer.Controllers
|
||||
(IClaimDestination dst, string err) = handler == null
|
||||
? (null, "No payment handler found for this payment method")
|
||||
: await handler.ParseAndValidateClaimDestination(pmId, vm.Destination, ppBlob, cancellationToken);
|
||||
error = err;
|
||||
if (dst is not null && err is null)
|
||||
{
|
||||
paymentMethodId = pmId;
|
||||
@ -247,12 +249,15 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
paymentMethodId = supported.FirstOrDefault(id => vm.SelectedPaymentMethod == id.ToString());
|
||||
var payoutHandler = paymentMethodId is null ? null : _payoutHandlers.FindPayoutHandler(paymentMethodId);
|
||||
destination = payoutHandler is null ? null : (await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken)).destination;
|
||||
if (payoutHandler is not null)
|
||||
{
|
||||
(destination, error) = await payoutHandler.ParseAndValidateClaimDestination(paymentMethodId, vm.Destination, ppBlob, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (destination is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.Destination), "Invalid destination or payment method");
|
||||
ModelState.AddModelError(nameof(vm.Destination), error ?? "Invalid destination or payment method");
|
||||
return await ViewPullPayment(pullPaymentId);
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,27 @@ public partial class UIReportsController
|
||||
decimal randomValue = ((decimal)rand.NextDouble() * range) + from;
|
||||
return decimal.Round(randomValue, precision);
|
||||
}
|
||||
|
||||
JObject GetFormattedAmount()
|
||||
{
|
||||
string? curr = null;
|
||||
decimal value = 0m;
|
||||
int offset = 0;
|
||||
while (curr is null)
|
||||
{
|
||||
curr = row[fi - 1 - offset]?.ToString();
|
||||
value = curr switch
|
||||
{
|
||||
"USD" or "EUR" or "CHF" => GenerateDecimal(30_000m, 60_000, 2),
|
||||
"JPY" => GenerateDecimal(400_0000m, 1000_0000m, 0),
|
||||
_ => 0.0m
|
||||
};
|
||||
if (value != 0.0m)
|
||||
break;
|
||||
curr = null;
|
||||
offset++;
|
||||
}
|
||||
return DisplayFormatter.ToFormattedAmount(value, curr);
|
||||
}
|
||||
var fiatCurrency = rand.NextSingle() > 0.2 ? "USD" : TakeOne("JPY", "EUR", "CHF");
|
||||
var cryptoCurrency = rand.NextSingle() > 0.2 ? "BTC" : TakeOne("LTC", "DOGE", "DASH");
|
||||
|
||||
@ -116,14 +136,11 @@ public partial class UIReportsController
|
||||
return Encoders.Hex.EncodeData(GenerateBytes(32));
|
||||
if (f.Name == "Rate")
|
||||
{
|
||||
var curr = row[fi - 1]?.ToString();
|
||||
var value = curr switch
|
||||
{
|
||||
"USD" or "EUR" or "CHF" => GenerateDecimal(30_000m, 60_000, 2),
|
||||
"JPY" => GenerateDecimal(400_0000m, 1000_0000m, 0),
|
||||
_ => GenerateDecimal(30_000m, 60_000, 2)
|
||||
};
|
||||
return DisplayFormatter.ToFormattedAmount(value, curr);
|
||||
return GetFormattedAmount();
|
||||
}
|
||||
if (f.Type == "amount")
|
||||
{
|
||||
return GetFormattedAmount();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
@ -16,13 +15,12 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Route("server/roles")]
|
||||
public async Task<IActionResult> ListRoles(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
RolesViewModel model,
|
||||
string sortOrder = null
|
||||
)
|
||||
{
|
||||
var roles = await storeRepository.GetStoreRoles(null, true);
|
||||
var defaultRole = (await storeRepository.GetDefaultRole()).Role;
|
||||
var roles = await _StoreRepository.GetStoreRoles(null, true);
|
||||
var defaultRole = (await _StoreRepository.GetDefaultRole()).Role;
|
||||
model ??= new RolesViewModel();
|
||||
model.DefaultRole = defaultRole;
|
||||
|
||||
@ -44,32 +42,26 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("server/roles/{role}")]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
public async Task<IActionResult> CreateOrEditRole(string role)
|
||||
{
|
||||
if (role == "create")
|
||||
{
|
||||
ModelState.Remove(nameof(role));
|
||||
return View(new UpdateRoleViewModel());
|
||||
}
|
||||
else
|
||||
|
||||
var roleData = await _StoreRepository.GetStoreRole(new StoreRoleId(role));
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
|
||||
return View(new UpdateRoleViewModel
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role));
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
return View(new UpdateRoleViewModel()
|
||||
{
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
Policies = roleData.Permissions,
|
||||
Role = roleData.Role
|
||||
});
|
||||
}
|
||||
[HttpPost("server/roles/{role}")]
|
||||
public async Task<IActionResult> CreateOrEditRole(
|
||||
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
[FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||
public async Task<IActionResult> CreateOrEditRole([FromRoute] string role, UpdateRoleViewModel viewModel)
|
||||
{
|
||||
string successMessage = null;
|
||||
if (role == "create")
|
||||
@ -80,7 +72,7 @@ namespace BTCPayServer.Controllers
|
||||
else
|
||||
{
|
||||
successMessage = "Role updated";
|
||||
var storeRole = await storeRepository.GetStoreRole(new StoreRoleId(role));
|
||||
var storeRole = await _StoreRepository.GetStoreRole(new StoreRoleId(role));
|
||||
if (storeRole == null)
|
||||
return NotFound();
|
||||
}
|
||||
@ -90,7 +82,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(viewModel);
|
||||
}
|
||||
|
||||
var r = await storeRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Policies);
|
||||
var r = await _StoreRepository.AddOrUpdateStoreRole(new StoreRoleId(role), viewModel.Policies);
|
||||
if (r is null)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
@ -113,11 +105,9 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
|
||||
[HttpGet("server/roles/{role}/delete")]
|
||||
public async Task<IActionResult> DeleteRole(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
public async Task<IActionResult> DeleteRole(string role)
|
||||
{
|
||||
var roleData = await storeRepository.GetStoreRole(new StoreRoleId(role), true);
|
||||
var roleData = await _StoreRepository.GetStoreRole(new StoreRoleId(role), true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
|
||||
@ -131,12 +121,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("server/roles/{role}/delete")]
|
||||
public async Task<IActionResult> DeleteRolePost(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
public async Task<IActionResult> DeleteRolePost(string role)
|
||||
{
|
||||
var roleId = new StoreRoleId(role);
|
||||
var roleData = await storeRepository.GetStoreRole(roleId, true);
|
||||
var roleData = await _StoreRepository.GetStoreRole(roleId, true);
|
||||
if (roleData == null)
|
||||
return NotFound();
|
||||
if (roleData.IsUsed is true)
|
||||
@ -144,7 +132,7 @@ namespace BTCPayServer.Controllers
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var errorMessage = await storeRepository.RemoveStoreRole(roleId);
|
||||
var errorMessage = await _StoreRepository.RemoveStoreRole(roleId);
|
||||
if (errorMessage is null)
|
||||
{
|
||||
|
||||
@ -159,19 +147,16 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("server/roles/{role}/default")]
|
||||
public async Task<IActionResult> SetDefaultRole(
|
||||
[FromServices] StoreRepository storeRepository,
|
||||
string role)
|
||||
public async Task<IActionResult> SetDefaultRole(string role)
|
||||
{
|
||||
var resolved = await storeRepository.ResolveStoreRoleId(null, role);
|
||||
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);
|
||||
await _StoreRepository.SetDefaultRole(role);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
|
||||
}
|
||||
|
||||
|
@ -10,10 +10,9 @@ using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -23,7 +22,6 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
|
||||
using PayoutData = BTCPayServer.Data.PayoutData;
|
||||
using PullPaymentData = BTCPayServer.Data.PullPaymentData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
@ -40,6 +38,8 @@ namespace BTCPayServer.Controllers
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly PayoutProcessorService _payoutProcessorService;
|
||||
private readonly IEnumerable<IPayoutProcessorFactory> _payoutProcessorFactories;
|
||||
|
||||
public StoreData CurrentStore
|
||||
{
|
||||
@ -55,6 +55,8 @@ namespace BTCPayServer.Controllers
|
||||
DisplayFormatter displayFormatter,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
PayoutProcessorService payoutProcessorService,
|
||||
IEnumerable<IPayoutProcessorFactory> payoutProcessorFactories,
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
@ -66,8 +68,10 @@ namespace BTCPayServer.Controllers
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_jsonSerializerSettings = jsonSerializerSettings;
|
||||
_authorizationService = authorizationService;
|
||||
_payoutProcessorService = payoutProcessorService;
|
||||
_payoutProcessorFactories = payoutProcessorFactories;
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("stores/{storeId}/pull-payments/new")]
|
||||
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> NewPullPayment(string storeId)
|
||||
@ -287,6 +291,7 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
|
||||
vm.PaymentMethods = await _payoutHandlers.GetSupportedPaymentMethods(HttpContext.GetStoreData());
|
||||
vm.HasPayoutProcessor = await HasPayoutProcessor(storeId, vm.PaymentMethodId);
|
||||
var paymentMethodId = PaymentMethodId.Parse(vm.PaymentMethodId);
|
||||
var handler = _payoutHandlers
|
||||
.FindPayoutHandler(paymentMethodId);
|
||||
@ -370,7 +375,7 @@ namespace BTCPayServer.Controllers
|
||||
break;
|
||||
}
|
||||
|
||||
if (command == "approve-pay")
|
||||
if (command == "approve-pay" && !vm.HasPayoutProcessor)
|
||||
{
|
||||
goto case "pay";
|
||||
}
|
||||
@ -486,16 +491,18 @@ namespace BTCPayServer.Controllers
|
||||
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
|
||||
}
|
||||
|
||||
paymentMethodId ??= paymentMethods.First().ToString();
|
||||
var vm = this.ParseListQuery(new PayoutsModel
|
||||
{
|
||||
PaymentMethods = paymentMethods,
|
||||
PaymentMethodId = paymentMethodId ?? paymentMethods.First().ToString(),
|
||||
PaymentMethodId = paymentMethodId,
|
||||
PullPaymentId = pullPaymentId,
|
||||
PayoutState = payoutState,
|
||||
Skip = skip,
|
||||
Count = count
|
||||
Count = count,
|
||||
Payouts = new List<PayoutsModel.PayoutModel>(),
|
||||
HasPayoutProcessor = await HasPayoutProcessor(storeId, paymentMethodId)
|
||||
});
|
||||
vm.Payouts = new List<PayoutsModel.PayoutModel>();
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payoutRequest =
|
||||
ctx.Payouts.Where(p => p.StoreDataId == storeId && (p.PullPaymentDataId == null || !p.PullPaymentData.Archived));
|
||||
@ -577,5 +584,13 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPayoutProcessor(string storeId, string paymentMethodId)
|
||||
{
|
||||
var pmId = PaymentMethodId.Parse(paymentMethodId);
|
||||
var processors = await _payoutProcessorService.GetProcessors(
|
||||
new PayoutProcessorService.PayoutProcessorQuery { Stores = [storeId], PaymentMethods = [paymentMethodId] });
|
||||
return _payoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmId)) && processors.Any();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ namespace BTCPayServer.Controllers
|
||||
[HttpGet("{storeId}/users")]
|
||||
public async Task<IActionResult> StoreUsers()
|
||||
{
|
||||
var vm = new StoreUsersViewModel { Role = StoreRoleId.Guest.Role };
|
||||
var vm = new StoreUsersViewModel { Role = StoreRoleId.Employee.Role };
|
||||
await FillUsers(vm);
|
||||
return View(vm);
|
||||
}
|
||||
|
@ -151,13 +151,12 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, WalletPSBTViewModel vm, string command = null)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
|
||||
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
|
||||
|
||||
if (psbt is null || vm.InvalidPSBT)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View("WalletSigningOptions", new WalletSigningOptionsModel
|
||||
{
|
||||
SigningContext = vm.SigningContext,
|
||||
@ -241,10 +240,9 @@ namespace BTCPayServer.Controllers
|
||||
vm.NBXSeedAvailable = await CanUseHotWallet() && derivationSchemeSettings.IsHotWallet;
|
||||
vm.BackUrl ??= HttpContext.Request.GetTypedHeaders().Referer?.AbsolutePath;
|
||||
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (vm.InvalidPSBT)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
if (psbt is null)
|
||||
@ -477,7 +475,7 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, WalletPSBTViewModel vm, string command, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
PSBT psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (vm.InvalidPSBT || psbt is null)
|
||||
{
|
||||
if (vm.InvalidPSBT)
|
||||
@ -637,16 +635,14 @@ namespace BTCPayServer.Controllers
|
||||
WalletId walletId, WalletPSBTCombineViewModel vm)
|
||||
{
|
||||
var network = NetworkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork);
|
||||
var psbt = await vm.GetPSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (psbt == null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.PSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork);
|
||||
if (sourcePSBT == null)
|
||||
var sourcePSBT = vm.GetSourcePSBT(network.NBitcoinNetwork, ModelState);
|
||||
if (sourcePSBT is null)
|
||||
{
|
||||
ModelState.AddModelError(nameof(vm.OtherPSBT), "Invalid PSBT");
|
||||
return View(vm);
|
||||
}
|
||||
sourcePSBT = sourcePSBT.Combine(psbt);
|
||||
|
@ -738,7 +738,7 @@ namespace BTCPayServer.Controllers
|
||||
foreach (var transactionOutput in vm.Outputs.Where(output => output.Labels?.Any() is true))
|
||||
{
|
||||
var labels = transactionOutput.Labels.Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
|
||||
var walletObjectAddress = new WalletObjectId(walletId, WalletObjectData.Types.Address, transactionOutput.DestinationAddress.ToLowerInvariant());
|
||||
var walletObjectAddress = new WalletObjectId(walletId, WalletObjectData.Types.Address, transactionOutput.DestinationAddress);
|
||||
var obj = await WalletRepository.GetWalletObject(walletObjectAddress);
|
||||
if (obj is null)
|
||||
{
|
||||
|
@ -11,7 +11,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
Bolt11 = bolt11 ?? throw new ArgumentNullException(nameof(bolt11));
|
||||
PaymentRequest = paymentRequest;
|
||||
PaymentHash = paymentRequest.Hash;
|
||||
Amount = paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||
Amount = paymentRequest.MinimumAmount.MilliSatoshi == LightMoney.Zero ? null: paymentRequest.MinimumAmount.ToDecimal(LightMoneyUnit.BTC);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
@ -37,11 +37,11 @@ namespace BTCPayServer.Filters
|
||||
}
|
||||
|
||||
// If we have an appId, we can redirect to the canonical domain
|
||||
if ((string)context.RouteContext.RouteData.Values["appId"] is { } appId && !req.IsOnion())
|
||||
if ((string)context.RouteContext.RouteData.Values["appId"] is { } appId)
|
||||
{
|
||||
var redirectDomain = mapping.FirstOrDefault(item => item.AppId == appId)?.Domain;
|
||||
// App is accessed via path, redirect to canonical domain
|
||||
if (!string.IsNullOrEmpty(redirectDomain) && req.Method != "POST" && !req.HasFormContentType)
|
||||
if (!string.IsNullOrEmpty(redirectDomain) && req.Method != "POST" && !req.HasFormContentType && !req.IsOnion())
|
||||
{
|
||||
var uri = new UriBuilder(req.Scheme, redirectDomain);
|
||||
if (req.Host.Port.HasValue)
|
||||
|
@ -7,11 +7,13 @@ using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Models.WalletViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -47,7 +49,7 @@ namespace BTCPayServer.HostedServices
|
||||
public class PullPaymentHostedService : BaseAsyncService
|
||||
{
|
||||
private readonly string[] _lnurlSupportedCurrencies = { "BTC", "SATS" };
|
||||
|
||||
|
||||
public class CancelRequest
|
||||
{
|
||||
public CancelRequest(string pullPaymentId)
|
||||
@ -107,7 +109,23 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<string> CreatePullPayment(string storeId, CreatePullPaymentRequest request)
|
||||
{
|
||||
return CreatePullPayment(new CreatePullPayment()
|
||||
{
|
||||
StartsAt = request.StartsAt,
|
||||
ExpiresAt = request.ExpiresAt,
|
||||
Period = request.Period,
|
||||
BOLT11Expiration = request.BOLT11Expiration,
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency,
|
||||
StoreId = storeId,
|
||||
PaymentMethodIds = request.PaymentMethods.Select(p => PaymentMethodId.Parse(p)).ToArray(),
|
||||
AutoApproveClaims = request.AutoApproveClaims
|
||||
});
|
||||
}
|
||||
public async Task<string> CreatePullPayment(CreatePullPayment create)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(create);
|
||||
@ -263,7 +281,7 @@ namespace BTCPayServer.HostedServices
|
||||
|
||||
return await query.FirstOrDefaultAsync(data => data.Id == pullPaymentId);
|
||||
}
|
||||
|
||||
record TopUpRequest(string PullPaymentId, InvoiceEntity InvoiceEntity);
|
||||
class PayoutRequest
|
||||
{
|
||||
public PayoutRequest(TaskCompletionSource<ClaimRequest.ClaimResponse> completionSource,
|
||||
@ -327,6 +345,17 @@ namespace BTCPayServer.HostedServices
|
||||
return new[] { Loop() };
|
||||
}
|
||||
|
||||
private void TopUpInvoice(InvoiceEvent evt)
|
||||
{
|
||||
if (evt.EventCode == InvoiceEventCode.Completed || evt.EventCode == InvoiceEventCode.MarkedCompleted)
|
||||
{
|
||||
foreach (var pullPaymentId in evt.Invoice.GetInternalTags("PULLPAY#"))
|
||||
{
|
||||
_Channel.Writer.TryWrite(new TopUpRequest(pullPaymentId, evt.Invoice));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Subscribe(params Type[] events)
|
||||
{
|
||||
foreach (Type @event in events)
|
||||
@ -339,6 +368,11 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
await foreach (var o in _Channel.Reader.ReadAllAsync())
|
||||
{
|
||||
if (o is TopUpRequest topUp)
|
||||
{
|
||||
await HandleTopUp(topUp);
|
||||
}
|
||||
|
||||
if (o is PayoutRequest req)
|
||||
{
|
||||
await HandleCreatePayout(req);
|
||||
@ -373,10 +407,44 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleTopUp(TopUpRequest topUp)
|
||||
{
|
||||
var pp = await this.GetPullPayment(topUp.PullPaymentId, false);
|
||||
var currency = pp.GetBlob().Currency;
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
|
||||
var payout = new Data.PayoutData()
|
||||
{
|
||||
Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(20)),
|
||||
Date = DateTimeOffset.UtcNow,
|
||||
State = PayoutState.Completed,
|
||||
PullPaymentDataId = pp.Id,
|
||||
PaymentMethodId = new PaymentMethodId("BTC", PaymentTypes.LightningLike).ToString(),
|
||||
Destination = null,
|
||||
StoreDataId = pp.StoreId
|
||||
};
|
||||
if (topUp.InvoiceEntity.Currency != currency ||
|
||||
currency is not ("SATS" or "BTC"))
|
||||
return;
|
||||
var paidAmount = topUp.InvoiceEntity.Price;
|
||||
var cryptoAmount = paidAmount;
|
||||
|
||||
var payoutBlob = new PayoutBlob()
|
||||
{
|
||||
CryptoAmount = -cryptoAmount,
|
||||
Amount = -paidAmount,
|
||||
Destination = topUp.InvoiceEntity.Id,
|
||||
Metadata = new JObject(),
|
||||
};
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.Payouts.AddAsync(payout);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public bool SupportsLNURL(PullPaymentBlob blob)
|
||||
{
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
|
||||
id.PaymentType == LightningPaymentType.Instance &&
|
||||
var pms = blob.SupportedPaymentMethods.FirstOrDefault(id =>
|
||||
id.PaymentType == LightningPaymentType.Instance &&
|
||||
_networkProvider.DefaultNetwork.CryptoCode == id.CryptoCode);
|
||||
return pms is not null && _lnurlSupportedCurrencies.Contains(blob.Currency);
|
||||
}
|
||||
@ -633,7 +701,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
Amount = claimed,
|
||||
Destination = req.ClaimRequest.Destination.ToString(),
|
||||
Metadata = req.ClaimRequest.Metadata?? new JObject(),
|
||||
Metadata = req.ClaimRequest.Metadata ?? new JObject(),
|
||||
};
|
||||
payout.SetBlob(payoutBlob, _jsonSerializerSettings);
|
||||
await ctx.Payouts.AddAsync(payout);
|
||||
@ -826,6 +894,10 @@ namespace BTCPayServer.HostedServices
|
||||
return time;
|
||||
}
|
||||
|
||||
public static string GetInternalTag(string id)
|
||||
{
|
||||
return $"PULLPAY#{id}";
|
||||
}
|
||||
|
||||
class InternalPayoutPaidRequest
|
||||
{
|
||||
@ -880,25 +952,25 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
null when destination.Amount is null && ppCurrency is null => ("Amount is not specified in destination or payout request", null),
|
||||
null when destination.Amount is null => (null, null),
|
||||
null when destination.Amount != null => (null,destination.Amount),
|
||||
not null when destination.Amount is null => (null,amount),
|
||||
null when destination.Amount != null => (null, destination.Amount),
|
||||
not null when destination.Amount is null => (null, amount),
|
||||
not null when destination.Amount != null && amount != destination.Amount &&
|
||||
destination.IsExplicitAmountMinimum &&
|
||||
payoutCurrency == "BTC" && ppCurrency == "SATS" &&
|
||||
new Money(amount.Value, MoneyUnit.Satoshi).ToUnit(MoneyUnit.BTC) < destination.Amount =>
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
|
||||
not null when destination.Amount != null && amount != destination.Amount &&
|
||||
destination.IsExplicitAmountMinimum &&
|
||||
!(payoutCurrency == "BTC" && ppCurrency == "SATS") &&
|
||||
amount < destination.Amount =>
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount",null),
|
||||
($"Amount is implied in both destination ({destination.Amount}) and payout request ({amount}), but the payout request amount is less than the destination amount", null),
|
||||
not null when destination.Amount != null && amount != destination.Amount &&
|
||||
!destination.IsExplicitAmountMinimum =>
|
||||
($"Amount is implied in destination ({destination.Amount}) that does not match the payout amount provided {amount})", null),
|
||||
_ => (null, amount)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public static string GetErrorMessage(ClaimResult result)
|
||||
{
|
||||
switch (result)
|
||||
|
@ -347,6 +347,8 @@ namespace BTCPayServer.Hosting
|
||||
htmlSanitizer.AllowedAttributes.Add("webkitallowfullscreen");
|
||||
htmlSanitizer.AllowedAttributes.Add("allowfullscreen");
|
||||
htmlSanitizer.AllowedSchemes.Add("mailto");
|
||||
htmlSanitizer.AllowedSchemes.Add("bitcoin");
|
||||
htmlSanitizer.AllowedSchemes.Add("lightning");
|
||||
return htmlSanitizer;
|
||||
});
|
||||
|
||||
@ -385,6 +387,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddReportProvider<ProductsReportProvider>();
|
||||
services.AddReportProvider<PayoutsReportProvider>();
|
||||
services.AddReportProvider<LegacyInvoiceExportReportProvider>();
|
||||
services.AddReportProvider<RefundsReportProvider>();
|
||||
services.AddWebhooks();
|
||||
services.AddSingleton<BitcoinLikePayoutHandler>();
|
||||
services.AddSingleton<IPayoutHandler>(provider => provider.GetRequiredService<BitcoinLikePayoutHandler>());
|
||||
|
@ -17,6 +17,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public decimal Amount { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public Dictionary<string, object> AdditionalData { get; set; }
|
||||
public Dictionary<string, object> CartData { get; set; }
|
||||
public ReceiptOptions ReceiptOptions { get; set; }
|
||||
public List<ViewPaymentRequestViewModel.PaymentRequestInvoicePayment> Payments { get; set; }
|
||||
public string OrderUrl { get; set; }
|
||||
|
@ -19,6 +19,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public IEnumerable<PaymentMethodId> PaymentMethods { get; set; }
|
||||
public PayoutState PayoutState { get; set; }
|
||||
public string PullPaymentName { get; set; }
|
||||
public bool HasPayoutProcessor { get; set; }
|
||||
|
||||
public class PayoutModel
|
||||
{
|
||||
|
@ -2,6 +2,7 @@ using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
@ -17,7 +18,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public string BackUrl { get; set; }
|
||||
public string ReturnUrl { get; set; }
|
||||
|
||||
public PSBT GetSourcePSBT(Network network)
|
||||
public PSBT GetSourcePSBT(Network network, ModelStateDictionary modelState)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OtherPSBT))
|
||||
{
|
||||
@ -25,12 +26,12 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
return NBitcoin.PSBT.Parse(OtherPSBT, network);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
catch (Exception ex)
|
||||
{ modelState.AddModelError(nameof(OtherPSBT), ex.Message); }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public async Task<PSBT> GetPSBT(Network network)
|
||||
public async Task<PSBT> GetPSBT(Network network, ModelStateDictionary modelState)
|
||||
{
|
||||
if (UploadedPSBTFile != null)
|
||||
{
|
||||
@ -45,8 +46,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
return NBitcoin.PSBT.Load(bytes, network);
|
||||
}
|
||||
catch
|
||||
catch (FormatException ex)
|
||||
{
|
||||
modelState.AddModelError(nameof(UploadedPSBTFile), ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -56,8 +58,10 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
{
|
||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
catch (FormatException ex)
|
||||
{
|
||||
modelState.AddModelError(nameof(UploadedPSBTFile), ex.Message);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
using NBitcoin;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels
|
||||
@ -35,9 +36,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
public IFormFile UploadedPSBTFile { get; set; }
|
||||
|
||||
|
||||
public async Task<PSBT> GetPSBT(Network network)
|
||||
public async Task<PSBT> GetPSBT(Network network, ModelStateDictionary modelState)
|
||||
{
|
||||
var psbt = await GetPSBTCore(network);
|
||||
var psbt = await GetPSBTCore(network, modelState);
|
||||
if (psbt != null)
|
||||
{
|
||||
Decoded = psbt.ToString();
|
||||
@ -52,7 +53,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
}
|
||||
public bool InvalidPSBT { get; set; }
|
||||
|
||||
async Task<PSBT> GetPSBTCore(Network network)
|
||||
async Task<PSBT> GetPSBTCore(Network network, ModelStateDictionary modelState)
|
||||
{
|
||||
if (UploadedPSBTFile != null)
|
||||
{
|
||||
@ -68,16 +69,20 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
}
|
||||
return NBitcoin.PSBT.Load(bytes, network);
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
using var stream = new StreamReader(UploadedPSBTFile.OpenReadStream());
|
||||
PSBT = await stream.ReadToEndAsync();
|
||||
modelState.Remove(nameof(PSBT));
|
||||
modelState.AddModelError(nameof(PSBT), ex.Message);
|
||||
InvalidPSBT = true;
|
||||
}
|
||||
}
|
||||
if (SigningContext != null && !string.IsNullOrEmpty(SigningContext.PSBT))
|
||||
{
|
||||
PSBT = SigningContext.PSBT;
|
||||
modelState.Remove(nameof(PSBT));
|
||||
InvalidPSBT = false;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(PSBT))
|
||||
{
|
||||
@ -86,8 +91,11 @@ namespace BTCPayServer.Models.WalletViewModels
|
||||
InvalidPSBT = false;
|
||||
return NBitcoin.PSBT.Parse(PSBT, network);
|
||||
}
|
||||
catch
|
||||
{ InvalidPSBT = true; }
|
||||
catch (Exception ex) when (!InvalidPSBT)
|
||||
{
|
||||
modelState.AddModelError(nameof(PSBT), ex.Message);
|
||||
InvalidPSBT = true;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -132,7 +132,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
// 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
|
||||
{
|
||||
@ -163,11 +163,14 @@ namespace BTCPayServer.Payments.Lightning
|
||||
? 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 && !(isLndHub && info.BlockHeight == 0))
|
||||
if (summary.Status is not null)
|
||||
{
|
||||
throw new PaymentMethodUnavailableException(
|
||||
$"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
|
||||
if (blocksGap > 10 && !(isLndHub && info.BlockHeight == 0))
|
||||
{
|
||||
throw new PaymentMethodUnavailableException(
|
||||
$"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
}
|
||||
}
|
||||
return nodeInfo;
|
||||
}
|
||||
|
@ -508,8 +508,15 @@ namespace BTCPayServer.Payments.Lightning
|
||||
try
|
||||
{
|
||||
var lightningClient = _lightningClientFactory.Create(ConnectionString, _network);
|
||||
if(lightningClient is null)
|
||||
return;
|
||||
uri = lightningClient.GetServerUri();
|
||||
logUrl = string.IsNullOrEmpty(uri.UserInfo) ? uri.ToString() : uri.ToString().Replace(uri.UserInfo, "***");
|
||||
logUrl = uri switch
|
||||
{
|
||||
null when LightningConnectionStringHelper.ExtractValues(ConnectionString, out var type) is not null => type,
|
||||
null => string.Empty,
|
||||
_ => string.IsNullOrEmpty(uri.UserInfo) ? uri.ToString() : uri.ToString().Replace(uri.UserInfo, "***")
|
||||
};
|
||||
Logs.PayServer.LogInformation("{CryptoCode} (Lightning): Start listening {Uri}", _network.CryptoCode, logUrl);
|
||||
using var session = await lightningClient.Listen(cancellation);
|
||||
// Just in case the payment arrived after our last poll but before we listened.
|
||||
|
@ -3,10 +3,10 @@ using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services.Altcoins.Monero.RPC.Models
|
||||
{
|
||||
public partial class SyncInfoResponse
|
||||
public partial class GetInfoResponse
|
||||
{
|
||||
[JsonProperty("height")] public long Height { get; set; }
|
||||
[JsonProperty("peers")] public List<Peer> Peers { get; set; }
|
||||
[JsonProperty("busy_syncing")] public bool BusySyncing { get; set; }
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
[JsonProperty("target_height")] public long? TargetHeight { get; set; }
|
||||
}
|
@ -9,7 +9,8 @@ namespace BTCPayServer
|
||||
{
|
||||
private const char FilterSeparator = ',';
|
||||
private const char ValueSeparator = ':';
|
||||
|
||||
private static readonly string[] StripFilters = ["status", "exceptionstatus", "unusual", "includearchived", "appid", "startdate", "enddate"];
|
||||
|
||||
private readonly string _originalString;
|
||||
private readonly int _timezoneOffset;
|
||||
|
||||
@ -27,12 +28,18 @@ namespace BTCPayServer
|
||||
.Where(kv => kv.Length == 2)
|
||||
.Select(kv => new KeyValuePair<string, string>(UnifyKey(kv[0]), kv[1]))
|
||||
.ToMultiValueDictionary(o => o.Key, o => o.Value);
|
||||
|
||||
var val = splitted.FirstOrDefault(a => a.IndexOf(ValueSeparator, StringComparison.OrdinalIgnoreCase) == -1);
|
||||
TextSearch = val != null ? val.Trim() : string.Empty;
|
||||
// combine raw search term and filters which don't have a special UI (e.g. orderid)
|
||||
var textFilters = Filters
|
||||
.Where(f => !StripFilters.Contains(f.Key))
|
||||
.Select(f => string.Join(FilterSeparator, f.Value.Select(v => $"{f.Key}{ValueSeparator}{v}"))).ToList();
|
||||
TextFilters = textFilters.Any() ? string.Join(FilterSeparator, textFilters) : null;
|
||||
TextSearch = splitted.FirstOrDefault(a => a.IndexOf(ValueSeparator, StringComparison.OrdinalIgnoreCase) == -1)?.Trim();
|
||||
}
|
||||
|
||||
public string TextSearch { get; private set; }
|
||||
public string TextFilters { get; private set; }
|
||||
|
||||
public string TextCombined => string.Join(FilterSeparator, new []{ TextFilters, TextSearch }.Where(x => !string.IsNullOrEmpty(x)));
|
||||
|
||||
public MultiValueDictionary<string, string> Filters { get; }
|
||||
|
||||
@ -82,9 +89,10 @@ namespace BTCPayServer
|
||||
|
||||
public string WithoutSearchText()
|
||||
{
|
||||
return string.IsNullOrEmpty(TextSearch)
|
||||
? Finalize(ToString())
|
||||
: Finalize(ToString()).Replace(TextSearch, string.Empty);
|
||||
var txt = ToString();
|
||||
if (!string.IsNullOrEmpty(TextSearch)) txt = Finalize(txt.Replace(TextSearch, string.Empty));
|
||||
if (!string.IsNullOrEmpty(TextFilters)) txt = Finalize(txt.Replace(TextFilters, string.Empty));
|
||||
return Finalize(txt).Trim();
|
||||
}
|
||||
|
||||
public string[] GetFilterArray(string key)
|
||||
@ -144,7 +152,7 @@ namespace BTCPayServer
|
||||
|
||||
private static string Finalize(string str)
|
||||
{
|
||||
var value = str.TrimStart(FilterSeparator).TrimEnd(FilterSeparator);
|
||||
var value = str.Trim().TrimStart(FilterSeparator).TrimEnd(FilterSeparator);
|
||||
return string.IsNullOrEmpty(value) ? " " : value;
|
||||
}
|
||||
}
|
||||
|
@ -59,12 +59,12 @@ namespace BTCPayServer.Services.Altcoins.Monero.Services
|
||||
try
|
||||
{
|
||||
var daemonResult =
|
||||
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, SyncInfoResponse>("sync_info",
|
||||
await daemonRpcClient.SendCommandAsync<JsonRpcClient.NoRequestModel, GetInfoResponse>("get_info",
|
||||
JsonRpcClient.NoRequestModel.Instance);
|
||||
summary.TargetHeight = daemonResult.TargetHeight.GetValueOrDefault(0);
|
||||
summary.CurrentHeight = daemonResult.Height;
|
||||
summary.TargetHeight = summary.TargetHeight == 0 ? summary.CurrentHeight : summary.TargetHeight;
|
||||
summary.Synced = daemonResult.Height >= summary.TargetHeight && summary.CurrentHeight > 0;
|
||||
summary.Synced = !daemonResult.BusySyncing;
|
||||
summary.UpdatedAt = DateTime.UtcNow;
|
||||
summary.DaemonAvailable = true;
|
||||
}
|
||||
|
@ -104,14 +104,19 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
new SelectListItem(
|
||||
$"{account.AccountIndex} - {(string.IsNullOrEmpty(account.Label) ? "No label" : account.Label)}",
|
||||
account.AccountIndex.ToString(CultureInfo.InvariantCulture)));
|
||||
var settlementThresholdChoice = settings.InvoiceSettledConfirmationThreshold switch
|
||||
|
||||
var settlementThresholdChoice = MoneroLikeSettlementThresholdChoice.StoreSpeedPolicy;
|
||||
if (settings != null && settings.InvoiceSettledConfirmationThreshold is { } confirmations)
|
||||
{
|
||||
null => MoneroLikeSettlementThresholdChoice.StoreSpeedPolicy,
|
||||
0 => MoneroLikeSettlementThresholdChoice.ZeroConfirmation,
|
||||
1 => MoneroLikeSettlementThresholdChoice.AtLeastOne,
|
||||
10 => MoneroLikeSettlementThresholdChoice.AtLeastTen,
|
||||
_ => MoneroLikeSettlementThresholdChoice.Custom
|
||||
};
|
||||
settlementThresholdChoice = confirmations switch
|
||||
{
|
||||
0 => MoneroLikeSettlementThresholdChoice.ZeroConfirmation,
|
||||
1 => MoneroLikeSettlementThresholdChoice.AtLeastOne,
|
||||
10 => MoneroLikeSettlementThresholdChoice.AtLeastTen,
|
||||
_ => MoneroLikeSettlementThresholdChoice.Custom
|
||||
};
|
||||
}
|
||||
|
||||
return new MoneroLikePaymentMethodViewModel()
|
||||
{
|
||||
WalletFileFound = System.IO.File.Exists(fileAddress),
|
||||
@ -124,9 +129,11 @@ namespace BTCPayServer.Services.Altcoins.Monero.UI
|
||||
Accounts = accounts == null ? null : new SelectList(accounts, nameof(SelectListItem.Value),
|
||||
nameof(SelectListItem.Text)),
|
||||
SettlementConfirmationThresholdChoice = settlementThresholdChoice,
|
||||
CustomSettlementConfirmationThreshold = settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
|
||||
? settings.InvoiceSettledConfirmationThreshold
|
||||
: null
|
||||
CustomSettlementConfirmationThreshold =
|
||||
settings != null &&
|
||||
settlementThresholdChoice is MoneroLikeSettlementThresholdChoice.Custom
|
||||
? settings.InvoiceSettledConfirmationThreshold
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
|
133
BTCPayServer/Services/Reporting/RefundsReportProvider.cs
Normal file
133
BTCPayServer/Services/Reporting/RefundsReportProvider.cs
Normal file
@ -0,0 +1,133 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BTCPayServer.Services.Reporting
|
||||
{
|
||||
public class RefundsReportProvider : ReportProvider
|
||||
{
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
|
||||
private readonly DisplayFormatter _displayFormatter;
|
||||
|
||||
private ViewDefinition CreateDefinition()
|
||||
{
|
||||
return new ViewDefinition
|
||||
{
|
||||
Fields = new List<StoreReportResponse.Field>
|
||||
{
|
||||
new("Date", "datetime"),
|
||||
new("InvoiceId", "invoice_id"),
|
||||
new("Currency", "string"),
|
||||
new("Completed", "amount"),
|
||||
new("Awaiting", "amount"),
|
||||
new("Limit", "amount"),
|
||||
new("FullyPaid", "boolean")
|
||||
},
|
||||
Charts =
|
||||
{
|
||||
new ()
|
||||
{
|
||||
Name = "Aggregated amount",
|
||||
Groups = { "Currency" },
|
||||
HasGrandTotal = false,
|
||||
Aggregates = { "Awaiting", "Completed", "Limit" }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
public override string Name => "Refunds";
|
||||
|
||||
public ApplicationDbContextFactory DbContextFactory { get; }
|
||||
|
||||
public RefundsReportProvider(
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkJsonSerializerSettings serializerSettings,
|
||||
DisplayFormatter displayFormatter)
|
||||
{
|
||||
DbContextFactory = dbContextFactory;
|
||||
_serializerSettings = serializerSettings;
|
||||
_displayFormatter = displayFormatter;
|
||||
}
|
||||
record RefundRow(DateTimeOffset Created, string InvoiceId, string PullPaymentId, string Currency, decimal Limit)
|
||||
{
|
||||
public decimal Completed { get; set; }
|
||||
public decimal Awaiting { get; set; }
|
||||
}
|
||||
public override async Task Query(QueryContext queryContext, CancellationToken cancellation)
|
||||
{
|
||||
queryContext.ViewDefinition = CreateDefinition();
|
||||
RefundRow? currentRow = null;
|
||||
await using var ctx = DbContextFactory.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
var rows = await conn.QueryAsync(
|
||||
"""
|
||||
SELECT i."Created", i."Id" AS "InvoiceId", p."State", p."PaymentMethodId", pp."Id" AS "PullPaymentId", pp."Blob" AS "ppBlob", p."Blob" AS "pBlob" FROM "Invoices" i
|
||||
JOIN "Refunds" r ON r."InvoiceDataId"= i."Id"
|
||||
JOIN "PullPayments" pp ON r."PullPaymentDataId"=pp."Id"
|
||||
LEFT JOIN "Payouts" p ON p."PullPaymentDataId"=pp."Id"
|
||||
WHERE i."StoreDataId" = @storeId
|
||||
AND i."Created" >= @start AND i."Created" <= @end
|
||||
AND pp."Archived" IS FALSE
|
||||
ORDER BY i."Created", pp."Id"
|
||||
""", new { start = queryContext.From, end = queryContext.To, storeId = queryContext.StoreId });
|
||||
foreach (var r in rows)
|
||||
{
|
||||
PullPaymentBlob ppBlob = GetPullPaymentBlob(r);
|
||||
PayoutBlob? pBlob = GetPayoutBlob(r);
|
||||
|
||||
if ((string)r.PullPaymentId != currentRow?.PullPaymentId)
|
||||
{
|
||||
AddRow(queryContext, currentRow);
|
||||
currentRow = new(r.Created, r.InvoiceId, r.PullPaymentId, ppBlob.Currency, ppBlob.Limit);
|
||||
}
|
||||
if (pBlob is null)
|
||||
continue;
|
||||
var state = Enum.Parse<PayoutState>((string)r.State);
|
||||
if (state == PayoutState.Cancelled)
|
||||
continue;
|
||||
if (state is PayoutState.Completed)
|
||||
currentRow.Completed += pBlob.Amount;
|
||||
else
|
||||
currentRow.Awaiting += pBlob.Amount;
|
||||
}
|
||||
AddRow(queryContext, currentRow);
|
||||
}
|
||||
|
||||
private PayoutBlob? GetPayoutBlob(dynamic r)
|
||||
{
|
||||
if (r.pBlob is null)
|
||||
return null;
|
||||
Data.PayoutData p = new Data.PayoutData();
|
||||
p.PaymentMethodId = r.PaymentMethodId;
|
||||
p.Blob = (string)r.pBlob;
|
||||
return p.GetBlob(_serializerSettings);
|
||||
}
|
||||
|
||||
private static PullPaymentBlob GetPullPaymentBlob(dynamic r)
|
||||
{
|
||||
Data.PullPaymentData pp = new Data.PullPaymentData();
|
||||
pp.Blob = (string)r.ppBlob;
|
||||
return pp.GetBlob();
|
||||
}
|
||||
|
||||
private void AddRow(QueryContext queryContext, RefundRow? currentRow)
|
||||
{
|
||||
if (currentRow is null)
|
||||
return;
|
||||
var data = queryContext.AddData();
|
||||
data.Add(currentRow.Created);
|
||||
data.Add(currentRow.InvoiceId);
|
||||
data.Add(currentRow.Currency);
|
||||
data.Add(_displayFormatter.ToFormattedAmount(currentRow.Completed, currentRow.Currency));
|
||||
data.Add(_displayFormatter.ToFormattedAmount(currentRow.Awaiting, currentRow.Currency));
|
||||
data.Add(_displayFormatter.ToFormattedAmount(currentRow.Limit, currentRow.Currency));
|
||||
data.Add(currentRow.Limit <= currentRow.Completed);
|
||||
}
|
||||
}
|
||||
}
|
@ -91,7 +91,7 @@ namespace BTCPayServer.Services.Wallets.Export
|
||||
{
|
||||
using StringWriter writer = new();
|
||||
using var csvWriter = new CsvHelper.CsvWriter(writer, new CsvConfiguration(CultureInfo.InvariantCulture), true);
|
||||
csvWriter.Configuration.RegisterClassMap<ExportTransactionMap>();
|
||||
csvWriter.Context.RegisterClassMap<ExportTransactionMap>();
|
||||
csvWriter.WriteHeader<ExportTransaction>();
|
||||
csvWriter.NextRecord();
|
||||
csvWriter.WriteRecords(invoices);
|
||||
@ -105,7 +105,7 @@ namespace BTCPayServer.Services.Wallets.Export
|
||||
public ExportTransactionMap()
|
||||
{
|
||||
AutoMap(CultureInfo.InvariantCulture);
|
||||
Map(m => m.Labels).ConvertUsing(row => row.Labels == null ? string.Empty : string.Join(", ", row.Labels));
|
||||
Map(m => m.Labels).Convert(row => row.Value.Labels == null ? string.Empty : string.Join(", ", row.Value.Labels));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -326,7 +326,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -1,6 +1,7 @@
|
||||
@using BTCPayServer.Client
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Models.ServerViewModels
|
||||
@model BTCPayServer.Models.EmailsViewModel
|
||||
|
||||
<div class="row">
|
||||
@ -44,7 +45,7 @@
|
||||
<div class="form-text">For many email providers (like Gmail) your login is your email address.</div>
|
||||
<span asp-validation-for="Settings.Login" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group" permission="@Policies.CanModifyStoreSettings">
|
||||
<div class="form-group" permission="@(Model is ServerEmailsViewModel ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings)">
|
||||
@if (!Model.PasswordSet)
|
||||
{
|
||||
<label asp-for="Settings.Password" class="form-label"></label>
|
||||
@ -81,7 +82,6 @@
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
|
||||
delegate('click', '#quick-fill .dropdown-menu a', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -163,13 +163,13 @@
|
||||
<span class="badge text-bg-warning inventory" v-if="item.inventory">
|
||||
{{ item.inventory > 0 ? `${item.inventory} left` : "Sold out" }}
|
||||
</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button type="button" v-on:click="updateQuantity(item.id, -1)" class="btn btn-light p-1 rounded-pill d-flex align-items-center justify-content-center">
|
||||
<vc:icon symbol="minus" />
|
||||
<div class="d-flex align-items-center gap-2 quantities">
|
||||
<button type="button" v-on:click="updateQuantity(item.id, -1)" class="btn btn-minus">
|
||||
<span><vc:icon symbol="minus" /></span>
|
||||
</button>
|
||||
<input class="form-control hide-number-spin w-50px" type="number" placeholder="Qty" min="1" step="1" :max="item.inventory" v-model.number="item.count">
|
||||
<button type="button" v-on:click="updateQuantity(item.id, +1)" class="btn btn-light p-1 rounded-pill d-flex align-items-center justify-content-center">
|
||||
<vc:icon symbol="plus" />
|
||||
<input class="form-control hide-number-spin w-50px text-center" type="number" placeholder="Qty" min="1" step="1" :max="item.inventory" v-model.number="item.count">
|
||||
<button type="button" v-on:click="updateQuantity(item.id, +1)" class="btn btn-plus">
|
||||
<span><vc:icon symbol="plus" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -115,8 +115,8 @@
|
||||
<div class="d-flex align-items-start w-100 gap-3">
|
||||
@if (!string.IsNullOrWhiteSpace(item.Image))
|
||||
{
|
||||
<div class="img">
|
||||
<img src="@item.Image" alt="@item.Title" />
|
||||
<div class="img d-none d-sm-block">
|
||||
<img src="@item.Image" alt="@item.Title" asp-append-version="true" />
|
||||
</div>
|
||||
}
|
||||
<div class="d-flex flex-column gap-2">
|
||||
@ -138,13 +138,13 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 ms-auto" v-if="inStock(@index)">
|
||||
<button type="button" v-on:click="updateQuantity(`@Safe.Raw(item.Id)`, -1, true)" class="btn btn-light p-1 rounded-pill d-flex align-items-center justify-content-center btn-minus" :disabled="getQuantity(`@Safe.Raw(item.Id)`) <= 0">
|
||||
<vc:icon symbol="minus" />
|
||||
<div class="d-flex align-items-center gap-2 ms-auto quantities">
|
||||
<button type="button" v-on:click="updateQuantity(`@Safe.Raw(item.Id)`, -1, true)" class="btn btn-minus" :disabled="getQuantity(`@Safe.Raw(item.Id)`) <= 0">
|
||||
<span><vc:icon symbol="minus" /></span>
|
||||
</button>
|
||||
<div class="quantity text-center fs-6" style="width:2rem">{{ getQuantity(`@Safe.Raw(item.Id)`) }}</div>
|
||||
<button type="button" v-on:click="updateQuantity(`@Safe.Raw(item.Id)`, +1, true)" class="btn btn-light p-1 rounded-pill d-flex align-items-center justify-content-center btn-plus">
|
||||
<vc:icon symbol="plus" />
|
||||
<div class="quantity text-center fs-5" style="width:2rem">{{ getQuantity(`@Safe.Raw(item.Id)`) }}</div>
|
||||
<button type="button" v-on:click="updateQuantity(`@Safe.Raw(item.Id)`, +1, true)" class="btn btn-plus" :disabled="!inStock(@index)">
|
||||
<span><vc:icon symbol="plus" /></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -304,7 +304,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -1,3 +1,4 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model (Dictionary<string, object> Items, int Level)
|
||||
|
||||
@functions {
|
||||
@ -10,14 +11,20 @@
|
||||
|
||||
@if (Model.Items.Any())
|
||||
{
|
||||
var hasCart = Model.Items.ContainsKey("Cart");
|
||||
@* Use titlecase and lowercase versions for backwards-compatibility *@
|
||||
string[] cartKeys = ["cart", "subtotal", "discount", "tip", "total"];
|
||||
<table class="table my-0" v-pre>
|
||||
@if (hasCart || (Model.Items.ContainsKey("Subtotal") && Model.Items.ContainsKey("Total")))
|
||||
@if (Model.Items.Keys.Any(key => cartKeys.Contains(key.ToLowerInvariant())))
|
||||
{
|
||||
@if (hasCart)
|
||||
_ = Model.Items.TryGetValue("cart", out var cart) || Model.Items.TryGetValue("Cart", out cart);
|
||||
var hasTotal = Model.Items.TryGetValue("total", out var total) || Model.Items.TryGetValue("Total", out total);
|
||||
var hasSubtotal = Model.Items.TryGetValue("subtotal", out var subtotal) || Model.Items.TryGetValue("subTotal", out subtotal) || Model.Items.TryGetValue("Subtotal", out subtotal);
|
||||
var hasDiscount = Model.Items.TryGetValue("discount", out var discount) || Model.Items.TryGetValue("Discount", out discount);
|
||||
var hasTip = Model.Items.TryGetValue("tip", out var tip) || Model.Items.TryGetValue("Tip", out tip);
|
||||
if (cart is Dictionary<string, object> { Keys.Count: > 0 } cartDict)
|
||||
{
|
||||
<tbody>
|
||||
@foreach (var (key, value) in (Dictionary<string, object>)Model.Items["Cart"])
|
||||
@foreach (var (key, value) in cartDict)
|
||||
{
|
||||
<tr>
|
||||
<th>@key</th>
|
||||
@ -26,35 +33,46 @@
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
<tfoot style="border-top-width:@(hasCart ? "3px" : "0")">
|
||||
@if (Model.Items.ContainsKey("Subtotal"))
|
||||
else if (cart is ICollection<object> { Count: > 0 } cartCollection)
|
||||
{
|
||||
<tbody>
|
||||
@foreach (var value in cartCollection)
|
||||
{
|
||||
<tr>
|
||||
<th>Subtotal</th>
|
||||
<td class="text-end">@Model.Items["Subtotal"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Items.ContainsKey("Discount"))
|
||||
{
|
||||
<tr>
|
||||
<th>Discount</th>
|
||||
<td class="text-end">@Model.Items["Discount"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Items.ContainsKey("Tip"))
|
||||
{
|
||||
<tr>
|
||||
<th>Tip</th>
|
||||
<td class="text-end">@Model.Items["Tip"]</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Items.ContainsKey("Total"))
|
||||
{
|
||||
<tr style="border-top-width:3px">
|
||||
<th>Total</th>
|
||||
<td class="text-end">@Model.Items["Total"]</td>
|
||||
<td>@value</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
<tfoot style="border-top-width:0">
|
||||
@if (hasSubtotal && (hasDiscount || hasTip))
|
||||
{
|
||||
<tr style="border-top-width:3px">
|
||||
<th>Subtotal</th>
|
||||
<td class="text-end">@subtotal</td>
|
||||
</tr>
|
||||
}
|
||||
@if (hasDiscount)
|
||||
{
|
||||
<tr>
|
||||
<th>Discount</th>
|
||||
<td class="text-end">@discount</td>
|
||||
</tr>
|
||||
}
|
||||
@if (hasTip)
|
||||
{
|
||||
<tr>
|
||||
<th>Tip</th>
|
||||
<td class="text-end">@tip</td>
|
||||
</tr>
|
||||
}
|
||||
@if (hasTotal)
|
||||
{
|
||||
<tr style="border-top-width:3px">
|
||||
<th>Total</th>
|
||||
<td class="text-end">@total</td>
|
||||
</tr>
|
||||
}
|
||||
</tfoot>
|
||||
}
|
||||
else
|
||||
|
@ -153,17 +153,11 @@
|
||||
<div class="col-xl-5 offcanvas-xl offcanvas-end" tabindex="-1" ref="editorOffcanvas">
|
||||
<div class="offcanvas-header p-3">
|
||||
<h5 class="offcanvas-title">Edit Item</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" v-on:click="hideOffcanvas">
|
||||
<vc:icon symbol="close" />
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm rounded-pill" :class="{ 'btn-primary': itemChanged, 'btn-outline-secondary': !itemChanged }" v-on:click="hideOffcanvas" v-text="itemChanged ? 'Apply' : 'Close'"></button>
|
||||
</div>
|
||||
<div class="offcanvas-body p-3 p-xl-0">
|
||||
<item-editor ref="itemEditor" :item="selectedItem" class="bg-tile w-100 p-xl-4 rounded" />
|
||||
</div>
|
||||
<div class="offcanvas-header p-3">
|
||||
<button class="btn btn-primary" type="button" v-on:click="() => { $refs.itemEditor.apply(); hideOffcanvas() }">Apply and close</button>
|
||||
<button class="btn btn-secondary" type="button" v-on:click="hideOffcanvas">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,13 +1,19 @@
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Components.MainLogo
|
||||
@using System.Text
|
||||
@inject SettingsRepository SettingsRepository
|
||||
@{
|
||||
Layout = "_LayoutSimple";
|
||||
ViewBag.ShowTitle ??= true;
|
||||
ViewBag.ShowLeadText ??= false;
|
||||
|
||||
// obfuscate email on public page, decode via JS (see below)
|
||||
var settings = await SettingsRepository.GetSettingAsync<ServerSettings>() ?? new ServerSettings();
|
||||
var contactUrl = settings.ContactUrl;
|
||||
if (contactUrl?.StartsWith("mailto:") is true)
|
||||
{
|
||||
contactUrl = Convert.ToBase64String(Encoding.UTF8.GetBytes(contactUrl));
|
||||
}
|
||||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
@ -64,11 +70,20 @@
|
||||
@RenderBody()
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(settings.ContactUrl))
|
||||
@if (!string.IsNullOrWhiteSpace(contactUrl))
|
||||
{
|
||||
<p class="text-center mt-n5 mb-5 pt-2">
|
||||
<a class="text-secondary" href="@settings.ContactUrl" id="ContactLink">Contact Us</a>
|
||||
<a class="text-secondary" href="@contactUrl" id="ContactLink">Contact Us</a>
|
||||
</p>
|
||||
@if (contactUrl != settings.ContactUrl)
|
||||
{
|
||||
<script>
|
||||
(function() {
|
||||
const link = document.getElementById('ContactLink')
|
||||
link.setAttribute('href', atob(link.getAttribute('href')))
|
||||
})()
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="row justify-content-center mt-5">
|
||||
|
@ -18,7 +18,10 @@
|
||||
@RenderBody()
|
||||
</div>
|
||||
</section>
|
||||
<partial name="_Footer"/>
|
||||
@if (ViewData["ShowFooter"] is not false)
|
||||
{
|
||||
<partial name="_Footer"/>
|
||||
}
|
||||
<partial name="LayoutFoot" />
|
||||
@await RenderSectionAsync("PageFootContent", false)
|
||||
</body>
|
||||
|
@ -431,7 +431,7 @@
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
@if (Model.ReceiptData != null && Model.ReceiptData.Any())
|
||||
@if (Model.ReceiptData?.Any() is true)
|
||||
{
|
||||
<div>
|
||||
<h3 class="mb-3">
|
||||
@ -443,7 +443,7 @@
|
||||
<partial name="PosData" model="(Model.ReceiptData, 1)" />
|
||||
</div>
|
||||
}
|
||||
@if (Model.AdditionalData != null && Model.AdditionalData.Any())
|
||||
@if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
<div>
|
||||
<h3 class="mb-3">
|
||||
|
@ -35,8 +35,8 @@
|
||||
#InvoiceSummary { gap: var(--btcpay-space-l); }
|
||||
#PaymentDetails table tbody tr:first-child td { padding-top: 1rem; }
|
||||
#PaymentDetails table tbody:not(:last-child) tr:last-child > th,td { padding-bottom: 1rem; }
|
||||
#posData td > table:last-child { margin-bottom: 0 !important; }
|
||||
#posData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
|
||||
#AdditionalData td > table:last-child, #CartData td > table:last-child { margin-bottom: 0 !important; }
|
||||
#AdditionalData table > tbody > tr:first-child > td > h4, #CartData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-vh-100">
|
||||
@ -62,7 +62,7 @@
|
||||
{
|
||||
if (Model.ReceiptOptions.ShowQR is true)
|
||||
{
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()"></vc:qr-code>
|
||||
<vc:qr-code data="@Context.Request.GetCurrentUrl()" />
|
||||
}
|
||||
<div class="d-flex gap-4 mb-0 flex-fill">
|
||||
<dl class="d-flex flex-column gap-4 mb-0 flex-fill">
|
||||
@ -102,6 +102,15 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (Model.CartData?.Any() is true)
|
||||
{
|
||||
<div id="CartData" class="tile">
|
||||
<h2 class="h4 mb-3">Cart</h2>
|
||||
<div class="table-responsive my-0">
|
||||
<partial name="PosData" model="(Model.CartData, 1)" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
if (Model.Payments?.Any() is true)
|
||||
{
|
||||
<div id="PaymentDetails" class="tile">
|
||||
|
@ -1,4 +1,17 @@
|
||||
@model BTCPayServer.Models.InvoicingModels.InvoiceReceiptViewModel
|
||||
|
||||
@functions {
|
||||
public bool IsManualEntryCart(Dictionary<string, object> additionalData)
|
||||
{
|
||||
_ = additionalData.TryGetValue("cart", out var data) || additionalData.TryGetValue("Cart", out data);
|
||||
if (data is Dictionary<string, object> cart)
|
||||
{
|
||||
return cart.Count == 1 && cart.ContainsKey("Manual entry 1");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@using BTCPayServer.Client.Models
|
||||
@using BTCPayServer.Components.QRCode
|
||||
@using BTCPayServer.Services
|
||||
@ -90,6 +103,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
var hasCart = Model.CartData?.Any() is true;
|
||||
<div id="PaymentDetails">
|
||||
<div class="my-2 text-center small">
|
||||
@if (!string.IsNullOrEmpty(Model.OrderId))
|
||||
@ -99,19 +113,29 @@
|
||||
@Model.Timestamp.ToBrowserDate()
|
||||
</div>
|
||||
<table class="table table-borderless table-sm small my-0">
|
||||
<tr>
|
||||
<td class="text-nowrap text-secondary">Total</td>
|
||||
<td class="text-end fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
@if (Model.AdditionalData?.Any() is true &&
|
||||
(Model.AdditionalData.ContainsKey("Cart") || Model.AdditionalData.ContainsKey("Discount") || Model.AdditionalData.ContainsKey("Tip")))
|
||||
@if (Model.AdditionalData?.Any() is true)
|
||||
{
|
||||
@if (Model.AdditionalData.ContainsKey("Cart"))
|
||||
@foreach (var (key, value) in Model.AdditionalData)
|
||||
{
|
||||
@foreach (var (key, value) in (Dictionary<string, object>)Model.AdditionalData["Cart"])
|
||||
<tr>
|
||||
<td class="text-secondary">@key</td>
|
||||
<td class="text-end">@value</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
}
|
||||
@if (hasCart && !IsManualEntryCart(Model.AdditionalData))
|
||||
{
|
||||
_ = Model.CartData.TryGetValue("cart", out var cart) || Model.CartData.TryGetValue("Cart", out cart);
|
||||
var hasTotal = Model.CartData.TryGetValue("total", out var total) || Model.CartData.TryGetValue("Total", out total);
|
||||
var hasSubtotal = Model.CartData.TryGetValue("subtotal", out var subtotal) || Model.CartData.TryGetValue("subTotal", out subtotal) || Model.CartData.TryGetValue("Subtotal", out subtotal);
|
||||
var hasDiscount = Model.CartData.TryGetValue("discount", out var discount) || Model.CartData.TryGetValue("Discount", out discount);
|
||||
var hasTip = Model.CartData.TryGetValue("tip", out var tip) || Model.CartData.TryGetValue("Tip", out tip);
|
||||
if (cart is Dictionary<string, object> { Keys.Count: > 0 } cartDict)
|
||||
{
|
||||
@foreach (var (key, value) in cartDict)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">@key</td>
|
||||
@ -119,33 +143,62 @@
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
@if (Model.AdditionalData.ContainsKey("Subtotal"))
|
||||
else if (cart is ICollection<object> { Count: > 0 } cartCollection)
|
||||
{
|
||||
@foreach (var value in cartCollection)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-end">@value</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
if (hasSubtotal && (hasDiscount || hasTip))
|
||||
{
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-secondary">Subtotal</td>
|
||||
<td class="text-end">@Model.AdditionalData["Subtotal"]</td>
|
||||
<td class="text-end">@subtotal</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.AdditionalData.ContainsKey("Discount"))
|
||||
if (hasDiscount)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">Discount</td>
|
||||
<td class="text-end">@Model.AdditionalData["Discount"]</td>
|
||||
<td class="text-end">@discount</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.AdditionalData.ContainsKey("Tip"))
|
||||
if (hasTip)
|
||||
{
|
||||
<tr>
|
||||
<td class="text-secondary">Tip</td>
|
||||
<td class="text-end">@Model.AdditionalData["Tip"]</td>
|
||||
<td class="text-end">@tip</td>
|
||||
</tr>
|
||||
}
|
||||
if (hasTotal)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th class="text-secondary">Total</th>
|
||||
<td class="text-end fw-semibold">@total</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
<td class="text-nowrap text-secondary">Total</td>
|
||||
<td class="text-end fw-semibold">@DisplayFormatter.Currency(Model.Amount, Model.Currency, DisplayFormatter.CurrencyFormat.Symbol)</td>
|
||||
</tr>
|
||||
}
|
||||
@if (Model.Payments?.Any() is true)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="2"><hr class="w-100 my-0"/></td>
|
||||
</tr>
|
||||
@for (var i = 0; i < Model.Payments.Count; i++)
|
||||
{
|
||||
var payment = Model.Payments[i];
|
||||
@ -178,9 +231,9 @@
|
||||
@if (payment.Destination.Length > 69)
|
||||
{
|
||||
<span>
|
||||
<span>@payment.Destination[..30]</span>
|
||||
<span>@payment.Destination[..19]</span>
|
||||
<span>...</span>
|
||||
<span>@payment.Destination.Substring(payment.Destination.Length - 30, 30)</span>
|
||||
<span>@payment.Destination.Substring(payment.Destination.Length - 20, 20)</span>
|
||||
</span>
|
||||
}
|
||||
else
|
||||
@ -216,7 +269,9 @@
|
||||
<hr class="w-100 my-0 bg-none"/>
|
||||
</center>
|
||||
</body>
|
||||
<script src="~/main/utils.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
formatDateTimes();
|
||||
window.print();
|
||||
</script>
|
||||
</html>
|
||||
|
@ -144,7 +144,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -118,7 +118,10 @@
|
||||
</p>
|
||||
<dl class="mt-n1 mb-4" v-if="srvModel.amountCollected > 0 && srvModel.amountDue > 0">
|
||||
<div class="progress bg-light d-flex mb-3 d-print-none" style="height:5px">
|
||||
<div class="progress-bar bg-primary" role="progressbar" style="width:@(Model.AmountCollected/Model.Amount*100)%" v-bind:style="{ width: (srvModel.amountCollected/srvModel.amount*100) + '%' }"></div>
|
||||
@{
|
||||
var prcnt = Model.Amount == 0? 100: Model.AmountCollected / Model.Amount * 100;
|
||||
}
|
||||
<div class="progress-bar bg-primary" role="progressbar" style="width:@prcnt%" v-bind:style="{ width: (srvModel.amountCollected/srvModel.amount*100) + '%' }"></div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-3 align-items-center justify-content-between">
|
||||
<div class="d-flex d-print-inline-block flex-column gap-1">
|
||||
|
@ -74,7 +74,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -58,7 +58,7 @@
|
||||
<select class="form-select w-auto" asp-for="SelectedPaymentMethod" asp-items="Model.PaymentMethods.Select(id => new SelectListItem(id.ToPrettyString(), id.ToString()))"></select>
|
||||
}
|
||||
<button type="button" class="btn btn-secondary only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan destination with camera" id="scandestination-button">
|
||||
<i class="fa fa-camera"></i>
|
||||
<vc:icon symbol="scan-qr"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -202,7 +202,7 @@
|
||||
</p>
|
||||
@if (Model.LnurlEndpoint is not null)
|
||||
{
|
||||
<p id="BoltcardActions" style="visibility:hidden">
|
||||
<p id="BoltcardActions">
|
||||
<a id="SetupBoltcard" asp-action="SetupBoltcard" asp-controller="UIPullPayment" asp-route-pullPaymentId="@Model.Id" asp-route-command="configure-boltcard">
|
||||
Setup Boltcard
|
||||
</a>
|
||||
@ -226,14 +226,13 @@
|
||||
<script src="~/vendor/ur-registry/urlib.min.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const isAndroid = /(android)/i.test(navigator.userAgent);
|
||||
if (isAndroid) {
|
||||
var isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
if (isMobile) {
|
||||
document.getElementById("SetupBoltcard").setAttribute('target', '_blank');
|
||||
document.getElementById("SetupBoltcard").setAttribute('href', @Safe.Json(@Model.SetupDeepLink));
|
||||
document.getElementById("SetupBoltcard").setAttribute('href', @Safe.Json(Model.SetupDeepLink));
|
||||
document.getElementById("ResetBoltcard").setAttribute('target', '_blank');
|
||||
document.getElementById("ResetBoltcard").setAttribute('href', @Safe.Json(@Model.ResetDeepLink));
|
||||
document.getElementById("ResetBoltcard").setAttribute('href', @Safe.Json(Model.ResetDeepLink));
|
||||
}
|
||||
document.getElementById("BoltcardActions").style.visibility = "visible";
|
||||
|
||||
window.qrApp = initQRShow({});
|
||||
delegate('click', 'button[page-qr]', event => {
|
||||
|
@ -32,7 +32,7 @@
|
||||
</a>
|
||||
</h2>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a cheat-mode="true" class="btn btn-outline-info text-nowrap" asp-action="StoreReports" asp-route-fakeData="true" asp-route-viewName="@Model.Request?.ViewName">Create fake date</a>
|
||||
<a cheat-mode="true" class="btn btn-outline-info text-nowrap" asp-action="StoreReports" asp-route-fakeData="true" asp-route-viewName="@Model.Request?.ViewName">Create fake data</a>
|
||||
<button id="exportCSV" class="btn btn-primary text-nowrap" type="button">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -69,7 +69,7 @@
|
||||
<div>
|
||||
<label asp-for="CustomTheme" class="form-check-label"></label>
|
||||
<div class="text-muted">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#1-custom-themes" target="_blank" rel="noreferrer noopener">Adjust the design</a>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener">Adjust the design</a>
|
||||
of your BTCPay Server instance to your needs.
|
||||
</div>
|
||||
</div>
|
||||
|
@ -110,7 +110,7 @@
|
||||
<div class="accordion-body">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomCSSLink" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#2-bootstrap-themes" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
|
@ -7,8 +7,6 @@
|
||||
@model BTCPayServer.Models.WalletViewModels.PayoutsModel
|
||||
|
||||
@inject IEnumerable<IPayoutHandler> PayoutHandlers;
|
||||
@inject PayoutProcessorService _payoutProcessorService;
|
||||
@inject IEnumerable<IPayoutProcessorFactory> _payoutProcessorFactories;
|
||||
@{
|
||||
var storeId = Context.GetRouteValue("storeId") as string;
|
||||
ViewData.SetActivePage(StoreNavPages.Payouts, $"Payouts{(string.IsNullOrEmpty(Model.PullPaymentName) ? string.Empty : " for pull payment " + Model.PullPaymentName)}", Context.GetStoreData().Id);
|
||||
@ -24,15 +22,16 @@
|
||||
return;
|
||||
stateActions.AddRange(payoutHandler.GetPayoutSpecificActions().Where(pair => pair.Key == Model.PayoutState).SelectMany(pair => pair.Value));
|
||||
}
|
||||
|
||||
switch (Model.PayoutState)
|
||||
{
|
||||
case PayoutState.AwaitingApproval:
|
||||
if (!Model.HasPayoutProcessor) stateActions.Add(("approve-pay", "Approve & Send"));
|
||||
stateActions.Add(("approve", "Approve"));
|
||||
stateActions.Add(("approve-pay", "Approve & Send"));
|
||||
stateActions.Add(("cancel", "Cancel"));
|
||||
break;
|
||||
case PayoutState.AwaitingPayment:
|
||||
stateActions.Add(("pay", "Send"));
|
||||
if (!Model.HasPayoutProcessor) stateActions.Add(("pay", "Send"));
|
||||
stateActions.Add(("cancel", "Cancel"));
|
||||
stateActions.Add(("mark-paid", "Mark as already paid"));
|
||||
break;
|
||||
@ -87,11 +86,7 @@
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
@if (_payoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(paymentMethodId)) && !(await _payoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery()
|
||||
{
|
||||
Stores = new[] { storeId },
|
||||
PaymentMethods = new[] { Model.PaymentMethodId }
|
||||
})).Any())
|
||||
@if (!Model.HasPayoutProcessor)
|
||||
{
|
||||
<div class="alert alert-info mb-5" role="alert" permission="@Policies.CanModifyStoreSettings">
|
||||
<strong>Pro tip:</strong> There are supported but unconfigured Payout Processors for this payout payment method.<br/>
|
||||
|
@ -205,7 +205,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomLogo" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#checkout-page-themes" target="_blank" rel="noreferrer noopener">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomLogo" class="form-control" />
|
||||
@ -213,7 +213,7 @@
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<label asp-for="CustomCSS" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#checkout-page-themes" target="_blank" rel="noreferrer noopener">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener">
|
||||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
<input asp-for="CustomCSS" class="form-control" />
|
||||
|
@ -4,13 +4,13 @@
|
||||
@using BTCPayServer.Client
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model StoreUsersViewModel
|
||||
@inject IScopeProvider ScopeProvider
|
||||
@inject StoreRepository StoreRepository
|
||||
@{
|
||||
var storeId = Context.GetStoreData().Id;
|
||||
Layout = "../Shared/_NavLayout.cshtml";
|
||||
ViewData.SetActivePage(StoreNavPages.Users, "Store Users", Context.GetStoreData().Id);
|
||||
ViewData.SetActivePage(StoreNavPages.Users, "Store Users", storeId);
|
||||
var roles = new SelectList(
|
||||
await StoreRepository.GetStoreRoles(ScopeProvider.GetCurrentStoreId()),
|
||||
await StoreRepository.GetStoreRoles(storeId),
|
||||
nameof(StoreRepository.StoreRole.Id), nameof(StoreRepository.StoreRole.Role),
|
||||
Model.Role);
|
||||
}
|
||||
@ -26,8 +26,9 @@
|
||||
<div class="col-xxl-constrain">
|
||||
<h3 class="mb-3">@ViewData["Title"]</h3>
|
||||
<p>
|
||||
Give other registered BTCPay Server users access to your store.<br />
|
||||
Guests will not be able to see or modify the store settings.
|
||||
Give other registered BTCPay Server users access to your store. See the
|
||||
<a asp-controller="UIStores" asp-action="ListRoles" asp-route-storeId="@storeId">roles</a>
|
||||
for granted permissions.
|
||||
</p>
|
||||
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
|
@ -26,6 +26,7 @@
|
||||
<symbol id="home" viewBox="0 0 24 24" fill="none"><path d="M15.6923 19.3845H8.30766C6.27689 19.3845 4.61536 17.7229 4.61536 15.6922V8.30754C4.61536 6.27677 6.27689 4.61523 8.30766 4.61523H15.6923C17.723 4.61523 19.3846 6.27677 19.3846 8.30754V15.6922C19.3846 17.7229 17.723 19.3845 15.6923 19.3845Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M7.56921 11.938H9.04614L10.5846 14.1534L13.3538 9.72266L14.8923 11.938H16.2461" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||
<symbol id="hot-wallet" viewBox="0 0 32 32" fill="none"><path d="M26.5362 7.08746H25.9614V3.25512C25.9614 2.10542 25.3865 1.14734 24.6201 0.572488C23.8536 -0.00236247 22.7039 -0.193979 21.7458 -0.00236246L4.11707 5.36291C2.00929 5.93776 0.667969 7.85392 0.667969 9.96171V12.0695V12.836V27.3988C0.667969 30.0815 2.77575 32.1893 5.45839 32.1893H26.5362C29.2189 32.1893 31.3267 30.0815 31.3267 27.3988V12.0695C31.3267 9.38686 29.2189 7.08746 26.5362 7.08746ZM4.69192 7.08746L22.129 1.91381C22.5123 1.72219 23.0871 1.91381 23.4704 2.10542C23.8536 2.29704 24.0452 2.87189 24.0452 3.25512V7.08746H5.45839C4.88354 7.08746 4.5003 7.27908 3.92545 7.47069C4.11707 7.27908 4.5003 7.08746 4.69192 7.08746ZM29.4105 27.2072C29.4105 28.7402 28.0692 30.0815 26.5362 30.0815H5.45839C3.92545 30.0815 2.58414 28.7402 2.58414 27.2072V12.836V11.8779C2.58414 10.3449 3.92545 9.00362 5.45839 9.00362H26.5362C28.0692 9.00362 29.4105 10.3449 29.4105 11.8779V27.2072Z" fill="currentColor"/><path d="M25.9591 21.6487C27.0174 21.6487 27.8753 20.7908 27.8753 19.7326C27.8753 18.6743 27.0174 17.8164 25.9591 17.8164C24.9009 17.8164 24.043 18.6743 24.043 19.7326C24.043 20.7908 24.9009 21.6487 25.9591 21.6487Z" fill="currentColor"/></symbol>
|
||||
<symbol id="info" viewBox="0 0 24 24" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 22.39c5.739 0 10.39-4.651 10.39-10.39C22.39 6.261 17.74 1.61 12 1.61 6.261 1.61 1.61 6.26 1.61 12c0 5.739 4.651 10.39 10.39 10.39zm0-2.597a7.793 7.793 0 1 0 0-15.586 7.793 7.793 0 0 0 0 15.586z" fill="currentColor"/><path d="M12 6.805a1.299 1.299 0 1 0 0 2.597 1.299 1.299 0 0 0 0-2.597zM10.701 12s0-1.299 1.299-1.299S13.299 12 13.299 12v3.897s0 1.298-1.299 1.298-1.299-1.298-1.299-1.298z" fill="currentColor"/></symbol>
|
||||
<symbol id="contact" viewBox="0 0 24 24" fill="none"><g transform="scale(.65) translate(7, 7)" fill="currentColor"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 22.39c5.739 0 10.39-4.651 10.39-10.39C22.39 6.261 17.74 1.61 12 1.61 6.261 1.61 1.61 6.26 1.61 12c0 5.739 4.651 10.39 10.39 10.39zm0-2.597a7.793 7.793 0 1 0 0-15.586 7.793 7.793 0 0 0 0 15.586z"/><path d="M12 6.805a1.299 1.299 0 1 0 0 2.597 1.299 1.299 0 0 0 0-2.597zM10.701 12s0-1.299 1.299-1.299S13.299 12 13.299 12v3.897s0 1.298-1.299 1.298-1.299-1.298-1.299-1.298z"/></g></symbol>
|
||||
<symbol id="invoice-2" viewBox="0 0 16 18" fill="none"><path d="M1.16699 3V16.5429L3.50033 15.6286L5.83366 17L8.16699 15.6286L10.5003 17L12.8337 15.6286L15.167 16.5429V3C15.167 1.89543 14.2716 1 13.167 1H3.16699C2.06242 1 1.16699 1.89543 1.16699 3Z" stroke="currentColor" stroke-width="1.6"/><path d="M4.66699 5H11.667" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M4.66699 8.5H11.667" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M4.66699 12H8.66699" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></symbol>
|
||||
<symbol id="invoice-expired" viewBox="0 0 48 48" fill="none"><circle cx="24" cy="24" r="22.5" stroke="currentColor" stroke-width="3"/><path d="m17 31 14-14m-14 0 14 14" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||
<symbol id="invoice" viewBox="0 0 24 24" fill="none"><path d="M8.30766 4.61523H15.6923C17.723 4.61523 19.3846 6.27677 19.3846 8.30754V15.6922C19.3846 17.7229 17.723 19.3845 15.6923 19.3845H8.30766C6.27689 19.3845 4.61536 17.7229 4.61536 15.6922V8.30754C4.61536 6.27677 6.27689 4.61523 8.30766 4.61523Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"/><path d="M8.30774 8.92383H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.30774 12H15.6924" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.30774 15.0156H12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/></symbol>
|
||||
|
Before (image error) Size: 78 KiB After (image error) Size: 78 KiB |
@ -1 +1,12 @@
|
||||
<svg width="150" height="100" style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" version="1.1" id="svg587" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><style id="style324">.st2{fill:#ffc214}.st3{fill:#f9f185}.st0{fill:#222221}.st1{fill:#272425}</style><g id="g931" transform="matrix(.375 0 0 .375 -306.863 -123.51)"><path fill-rule="evenodd" clip-rule="evenodd" d="M911.118 436.635c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l56.773 21.793c.062.026.126.05.19.074l28.48 10.932c7.864 3.02 16.689-.909 19.706-8.774 3.02-7.865-.908-16.688-8.774-19.707v-.003l-28.48-10.932c-7.865-3.02-11.793-11.842-8.774-19.707 3.02-7.865 11.842-11.793 19.707-8.774l83.768 32.155c.2.077.399.158.594.242a84 84 0 0 1 1.08.406c39.325 15.095 58.967 59.21 43.87 98.535-15.095 39.324-59.21 58.965-98.534 43.87a78.402 78.402 0 0 1-2.249-.903c-.36-.11-.72-.232-1.076-.37l-82.117-31.521c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l28.477 10.931-.002-.007.006.002c7.865 3.02 16.688-.909 19.706-8.774 3.02-7.865-.908-16.688-8.773-19.707l-12.817-4.92v-.001z" fill="currentColor" id="path1" style="clip-rule:evenodd;fill:#000;fill-rule:evenodd;stroke-width:1.52532;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"/></g></svg>
|
||||
<svg width="150" height="100" style="clip-rule:evenodd;fill-rule:evenodd;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision" version="1.1" id="svg587" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="100%" height="100%" fill="#ffffff"/>
|
||||
<style id="style324">
|
||||
.st2{fill:#ffc214}
|
||||
.st3{fill:#f9f185}
|
||||
.st0{fill:#222221}
|
||||
.st1{fill:#272425}
|
||||
</style>
|
||||
<g id="g931" transform="matrix(.375 0 0 .375 -306.863 -123.51)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M911.118 436.635c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l56.773 21.793c.062.026.126.05.19.074l28.48 10.932c7.864 3.02 16.689-.909 19.706-8.774 3.02-7.865-.908-16.688-8.774-19.707v-.003l-28.48-10.932c-7.865-3.02-11.793-11.842-8.774-19.707 3.02-7.865 11.842-11.793 19.707-8.774l83.768 32.155c.2.077.399.158.594.242a84 84 0 0 1 1.08.406c39.325 15.095 58.967 59.21 43.87 98.535-15.095 39.324-59.21 58.965-98.534 43.87a78.402 78.402 0 0 1-2.249-.903c-.36-.11-.72-.232-1.076-.37l-82.117-31.521c-7.865-3.02-11.793-11.842-8.774-19.707s11.842-11.793 19.707-8.774l28.477 10.931-.002-.007.006.002c7.865 3.02 16.688-.909 19.706-8.774 3.02-7.865-.908-16.688-8.773-19.707l-12.817-4.92v-.001z" fill="currentColor" id="path1" style="clip-rule:evenodd;fill:#000;fill-rule:evenodd;stroke-width:1.52532;image-rendering:optimizeQuality;shape-rendering:geometricPrecision;text-rendering:geometricPrecision"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before (image error) Size: 1.4 KiB After (image error) Size: 1.4 KiB |
3
BTCPayServer/wwwroot/img/readme/supporter_tether.svg
Normal file
3
BTCPayServer/wwwroot/img/readme/supporter_tether.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="150" height="100" viewBox="0 0 150 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4825 0.862305H88.0496C89.5663 0.862305 90.9675 1.64827 91.7239 2.92338L110.244 34.1419C111.204 35.7609 110.919 37.8043 109.549 39.1171L58.5729 87.9703C56.9216 89.5528 54.2652 89.5528 52.6139 87.9703L1.70699 39.1831C0.305262 37.8398 0.0427812 35.7367 1.07354 34.1077L20.8696 2.82322C21.6406 1.60483 23.0087 0.862305 24.4825 0.862305ZM79.8419 14.8003V23.5597H61.7343V29.6329C74.4518 30.2819 83.9934 32.9475 84.0642 36.1425L84.0638 42.803C83.993 45.998 74.4518 48.6635 61.7343 49.3125V64.2168H49.7105V49.3125C36.9929 48.6635 27.4513 45.998 27.3805 42.803L27.381 36.1425C27.4517 32.9475 36.9929 30.2819 49.7105 29.6329V23.5597H31.6028V14.8003H79.8419ZM55.7224 44.7367C69.2943 44.7367 80.6382 42.4827 83.4143 39.4727C81.0601 36.9202 72.5448 34.9114 61.7343 34.3597V40.7183C59.7966 40.8172 57.7852 40.8693 55.7224 40.8693C53.6595 40.8693 51.6481 40.8172 49.7105 40.7183V34.3597C38.8999 34.9114 30.3846 36.9202 28.0304 39.4727C30.8066 42.4827 42.1504 44.7367 55.7224 44.7367Z" fill="#009393" transform="translate(20, 5)"/>
|
||||
</svg>
|
After (image error) Size: 1.2 KiB |
@ -185,7 +185,7 @@ function downloadCSV() {
|
||||
|
||||
// Convert ISO8601 dates to YYYY-MM-DD HH:mm:ss so the CSV easily integrate with Excel
|
||||
modifyFields(srv.result.fields, data, 'amount', displayValue)
|
||||
modifyFields(srv.result.fields, data, 'datetime', v => v? moment(v).format('YYYY-MM-DD hh:mm:ss'): v);
|
||||
modifyFields(srv.result.fields, data, 'datetime', v => v ? moment(v).format('YYYY-MM-DD HH:mm:ss') : v);
|
||||
const csv = Papa.unparse({ fields: srv.result.fields.map(f => f.name), data });
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
saveAs(blob, "export.csv");
|
||||
|
@ -235,7 +235,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return {
|
||||
items,
|
||||
selectedItem: null,
|
||||
editorOffcanvas: null
|
||||
selectedItemInitial: null,
|
||||
editorOffcanvas: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -247,6 +248,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
(item.categories || []).forEach(category => { res.add(category); });
|
||||
return res;
|
||||
}, new Set()));
|
||||
},
|
||||
itemChanged() {
|
||||
return this.selectedItem && this.selectedItemInitial && (
|
||||
this.selectedItem.id !== this.selectedItemInitial.id ||
|
||||
this.selectedItem.title !== this.selectedItemInitial.title ||
|
||||
this.selectedItem.price !== this.selectedItemInitial.price ||
|
||||
this.selectedItem.image !== this.selectedItemInitial.image ||
|
||||
this.selectedItem.disabled !== this.selectedItemInitial.disabled ||
|
||||
this.selectedItem.inventory !== this.selectedItemInitial.inventory ||
|
||||
this.selectedItem.priceType !== this.selectedItemInitial.priceType ||
|
||||
this.selectedItem.categories !== this.selectedItemInitial.categories ||
|
||||
this.selectedItem.description !== this.selectedItemInitial.description
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -254,7 +268,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const items = parseConfig(event.target.value)
|
||||
if (!items) return
|
||||
this.items = items
|
||||
this.selectedItem = null
|
||||
this.selectedItem = this.selectedItemInitial = null
|
||||
},
|
||||
addItem(event) {
|
||||
const length = this.items.push({
|
||||
@ -268,16 +282,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
inventory: null,
|
||||
disabled: false
|
||||
})
|
||||
this.selectedItem = this.items[length - 1]
|
||||
this.showOffcanvas()
|
||||
this.selectItem(null, length - 1)
|
||||
},
|
||||
selectItem(event, index) {
|
||||
this.selectedItem = this.items[index]
|
||||
this.selectedItemInitial = { ...this.selectedItem } // pristine copy
|
||||
this.showOffcanvas()
|
||||
},
|
||||
removeItem(event, index) {
|
||||
this.items.splice(index, 1)
|
||||
this.selectedItem = null
|
||||
this.selectedItem = this.selectedItemInitial = null
|
||||
},
|
||||
sortItems(event) {
|
||||
const { newIndex, oldIndex } = event
|
||||
|
@ -473,7 +473,7 @@ svg.icon-note {
|
||||
font-weight: var(--btcpay-font-weight-bold);
|
||||
}
|
||||
|
||||
.widget .table {
|
||||
.widget *:not([class*='table-responsive']) > .table {
|
||||
margin-left: -.5rem;
|
||||
margin-right: -.5rem;
|
||||
width: calc(100% + 1rem);
|
||||
|
@ -2,19 +2,6 @@ const baseUrl = Object.values(document.scripts).find(s => s.src.includes('/main/
|
||||
|
||||
const flatpickrInstances = [];
|
||||
|
||||
const formatDateTimes = format => {
|
||||
// select only elements which haven't been initialized before, those without data-localized
|
||||
document.querySelectorAll("time[datetime]:not([data-localized])").forEach($el => {
|
||||
const date = new Date($el.getAttribute("datetime"));
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
const { dateStyle = 'short', timeStyle = 'short' } = $el.dataset;
|
||||
// initialize and set localized attribute
|
||||
$el.dataset.localized = new Intl.DateTimeFormat('default', { dateStyle, timeStyle }).format(date);
|
||||
// set text to chosen mode
|
||||
const mode = format || $el.dataset.initial;
|
||||
if ($el.dataset[mode]) $el.innerText = $el.dataset[mode];
|
||||
});
|
||||
};
|
||||
|
||||
const switchTimeFormat = event => {
|
||||
const curr = event.target.dataset.mode || 'localized';
|
||||
@ -166,8 +153,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
|
||||
// initialize timezone offset value if field is present in page
|
||||
var timezoneOffset = new Date().getTimezoneOffset();
|
||||
$("#TimezoneOffset").val(timezoneOffset);
|
||||
const $timezoneOffset = document.getElementById("TimezoneOffset");
|
||||
const timezoneOffset = new Date().getTimezoneOffset();
|
||||
if ($timezoneOffset) $timezoneOffset.value = timezoneOffset;
|
||||
|
||||
// localize all elements that have localizeDate class
|
||||
formatDateTimes();
|
||||
|
@ -15,3 +15,17 @@ function debounce(key, fn, delay = 250) {
|
||||
clearTimeout(DEBOUNCE_TIMERS[key])
|
||||
DEBOUNCE_TIMERS[key] = setTimeout(fn, delay)
|
||||
}
|
||||
|
||||
function formatDateTimes(format) {
|
||||
// select only elements which haven't been initialized before, those without data-localized
|
||||
document.querySelectorAll("time[datetime]:not([data-localized])").forEach($el => {
|
||||
const date = new Date($el.getAttribute("datetime"));
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
|
||||
const { dateStyle = 'short', timeStyle = 'short' } = $el.dataset;
|
||||
// initialize and set localized attribute
|
||||
$el.dataset.localized = new Intl.DateTimeFormat('default', { dateStyle, timeStyle }).format(date);
|
||||
// set text to chosen mode
|
||||
const mode = format || $el.dataset.initial;
|
||||
if ($el.dataset[mode]) $el.innerText = $el.dataset[mode];
|
||||
});
|
||||
}
|
||||
|
@ -100,14 +100,6 @@ header .cart-toggle-btn {
|
||||
background-color: var(--btcpay-bg-tile);
|
||||
}
|
||||
|
||||
#cart .quantity .btn {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
#cart .quantity .btn .icon{
|
||||
--btn-icon-size: .75rem;
|
||||
}
|
||||
|
||||
#CartBadge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -96,3 +96,30 @@
|
||||
max-height: 210px;
|
||||
object-fit: scale-down;
|
||||
}
|
||||
.quantities .btn {
|
||||
--btcpay-btn-disabled-opacity: .3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
background: none;
|
||||
border: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.quantities .btn span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
background: var(--btcpay-light);
|
||||
}
|
||||
.quantities .btn:hover span {
|
||||
background: var(--btcpay-light-bg-hover);
|
||||
}
|
||||
.quantities .icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"info": {
|
||||
"title": "BTCPay Greenfield API",
|
||||
"version": "v1",
|
||||
"description": "A full API to use your BTCPay Server",
|
||||
"description": "# Introduction\n\nThe BTCPay Server Greenfield API is a REST API. Our API has predictable resource-oriented URLs, accepts form-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs.\n\n# Authentication\n\nYou can authenticate either via Basic Auth or an API key. It's recommended to use an API key for better security. You can create an API key in the BTCPay Server UI under `Account` -> `Manage Account` -> `API keys`. You can restrict the API key for one or multiple stores and for specific permissions. For testing purposes, you can give it the 'Unrestricted access' permission. On production you should limit the permissions to the actual endpoints you use, you can see the required permission on the API docs at the top of each endpoint under `AUTHORIZATIONS`.\n\nIf you want to simplify the process of creating API keys for your users, you can use the [Authorization endpoint](https:\/\/docs.btcpayserver.org\/API\/Greenfield\/v1\/#tag\/Authorization) to predefine permissions and redirect your users to the BTCPay Server Authorization UI. You can find more information about this on the [API Authorization Flow docs](https:\/\/docs.btcpayserver.org\/BTCPayServer\/greenfield-authorization\/) page.\n\n# Usage examples\n\nUse **Basic Auth** to read store information with cURL:\n```bash\nBTCPAY_INSTANCE=\"https:\/\/mainnet.demo.btcpayserver.org\"\nUSER=\"MyTestUser@gmail.com\"\nPASSWORD=\"notverysecurepassword\"\nPERMISSION=\"btcpay.store.canmodifystoresettings\"\nBODY=\"$(echo \"{}\" | jq --arg \"a\" \"$PERMISSION\" '. + {permissions:[$a]}')\"\n\nAPI_KEY=\"$(curl -s \\\n -H \"Content-Type: application\/json\" \\\n --user \"$USER:$PASSWORD\" \\\n -X POST \\\n -d \"$BODY\" \\\n \"$BTCPAY_INSTANCE\/api\/v1\/api-keys\" | jq -r .apiKey)\"\n```\n\n\nUse an **API key** to read store information with cURL:\n```bash\nSTORE_ID=\"yourStoreId\"\n\ncurl -s \\\n -H \"Content-Type: application\/json\" \\\n -H \"Authorization: token $API_KEY\" \\\n -X GET \\\n \"$BTCPAY_INSTANCE\/api\/v1\/stores\/$STORE_ID\"\n```\n\nYou can find more examples on our docs for different programming languages:\n- [cURL](https:\/\/docs.btcpayserver.org\/Development\/GreenFieldExample\/)\n- [Javascript\/Node.Js](https:\/\/docs.btcpayserver.org\/Development\/GreenFieldExample-NodeJS\/)\n- [PHP](https:\/\/docs.btcpayserver.org\/Development\/GreenFieldExample-PHP\/)\n\n",
|
||||
"contact": {
|
||||
"name": "BTCPay Server",
|
||||
"url": "https://btcpayserver.org"
|
||||
|
@ -92,7 +92,7 @@
|
||||
"Users"
|
||||
],
|
||||
"summary": "Create user",
|
||||
"description": "Create a new user.\n\nThis operation can be called without authentication in any of this cases:\n* There is not any administrator yet on the server,\n* The subscriptions are not disabled in the server's policies.\n\nIf the first administrator is created by this call, subscriptions are automatically disabled.",
|
||||
"description": "Create a new user.\n\nThis operation can be called without authentication in any of this cases:\n* There is not any administrator yet on the server,\n* User registrations are not disabled in the server's policies.\n\nIf the first administrator is created by this call, user registrations are automatically disabled.",
|
||||
"requestBody": {
|
||||
"x-name": "request",
|
||||
"content": {
|
||||
@ -149,7 +149,7 @@
|
||||
"description": "If you need to authenticate for this endpoint (ie. the server settings policies lock subscriptions and that an admin already exists)"
|
||||
},
|
||||
"403": {
|
||||
"description": "If you are authenticated but forbidden to create a new user (ie. you don't have the `unrestricted` permission on a server administrator or if you are not administrator and registrations are disabled in the server's policies)"
|
||||
"description": "If you are authenticated but forbidden to create a new user (ie. you don't have the `unrestricted` permission on a server administrator or if you are not administrator and user registrations are disabled in the server's policies)"
|
||||
},
|
||||
"429": {
|
||||
"description": "DDoS protection if you are creating more than 2 accounts every minutes (non-admin only)"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<Version>1.13.0</Version>
|
||||
<Version>1.13.2</Version>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
50
Changelog.md
50
Changelog.md
@ -1,13 +1,52 @@
|
||||
# Changelog
|
||||
|
||||
## 1.13.2
|
||||
|
||||
### New features
|
||||
|
||||
* Add refund reports (#5791) @NicolasDorier
|
||||
* Allow `lightning:` in html hyperlinks (#6002 #6001) @dennisreimann
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* If you specified a 0 amount bolt 11 invoice for a payout, it would be incorrectly validated and not accepted. (#5943 #5819) @Kukks
|
||||
* Domain mapping constraint: Fix .onion case (#5948 #5917) @dennisreimann
|
||||
* Pull payment QR scan fixes (#5950) @dennisreimann
|
||||
* Server email settings: Fix missing password field (#5952 #5949) @dennisreimann
|
||||
* Fix: Some valid taproot PSBT couldn't parsed and show better error message (#5715 #5993) @NicolasDorier
|
||||
* Fix: Adding a label to a base58 addresses in the `Send Wallet` screen wasn't working (#6011) @NicolasDorier
|
||||
|
||||
### Improvements
|
||||
|
||||
* Search: Display text filters in search input (#5986 #5984) @dennisreimann
|
||||
* POS: Allow overpay for articles with minimum price (#5997 #5995) @dennisreimann
|
||||
* Improve data display on receipt (#5896 #5882) @dennisreimann
|
||||
* Greenfield API clarifications (#5955) @ndeet
|
||||
* Improvements to receipts display for PoS @rockstardev
|
||||
* Fix layout on mobile on the dashboard (#5721 #6006) @dennisreimann
|
||||
|
||||
## 1.13.1
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix: CSV exports from the Reports were exporting dates in 12-hour format instead of 24-hour format. (#5915, #5922) @TChukwuleta
|
||||
* Fix: Crash when configuring BTCPay Server with a non-default Postgres schema (Fix #5901) @NicolasDorier
|
||||
* Fix: A payment request with an amount of 0 no longer causes the payment request's page to crash (#5926) @Kukks
|
||||
|
||||
### Improvements
|
||||
|
||||
* Prevent unintentional double payouts (#5931, #5913) @dennisreimann
|
||||
* The `buyerEmail` field in a Payment Request's form will now set the email for the payment request (#5926) @Kukks
|
||||
* Added Tether as a supporter to the BTCPay Server Foundation (#5891) @rockstardev
|
||||
|
||||
## 1.13.0
|
||||
|
||||
### New feature
|
||||
|
||||
* Server Settings: Customize instance name and add contact URL (#5718) @dennisreimann
|
||||
* Server Settings: Customize instance name and add contact URL (#5718 #5872) @dennisreimann
|
||||
* Admin overview of the stores on the instance (#5745 #5782) @dennisreimann @Kukks
|
||||
* Onboarding: Invite new users (#5714 #5719) @dennisreimann @dstrukt
|
||||
* POS: Add item list to keypad (#5814) @dennisreimann @dstrukt
|
||||
* Onboarding: Invite new users (#5714 #5719 #5874) @dennisreimann @dstrukt
|
||||
* POS: Add item list to keypad (#5814 #5857 #5877) @dennisreimann @dstrukt
|
||||
* Wallet: Support BBQr PSBTSs (#5852) @Kukks
|
||||
|
||||
### Improvements
|
||||
@ -17,7 +56,7 @@
|
||||
* Wallet: Support 16mb PSBTs (#5768) @Kukks
|
||||
* Invoice: Improve events display (#5775) @dennisreimann
|
||||
* Crowdfund: Add forms (like with the POS) (#5659) @Nisaba
|
||||
* API docs: Add link to API usage examples in docs (#5772) @ndeet
|
||||
* API docs: Adding introduction, Authentication and Usage examples sections (#5772 #5858) @ndeet
|
||||
* Policies: Cleanup and improvements (#5731) @dennisreimann @dstrukt
|
||||
* Add legacy report (#5740) @Kukks
|
||||
* Store: Move support URL to Checkout Appearance and improve wording (#5717) @dennisreimann
|
||||
@ -33,7 +72,7 @@
|
||||
* Pull Payments: When opened in mobile, use deeplink to setup card (#5613) @NicolasDorier
|
||||
* UI consistency: Use toggles in various setting views (#5769) @TChukwuleta
|
||||
* Wallet: Improve info message (#5756) @rockstardev
|
||||
* Item Editor: Apply item changes directly (#5849) @dennisreimann
|
||||
* Item Editor: Apply item changes directly (#5849 #5871) @dennisreimann
|
||||
* Specify mailto: prefix for emails in Server Settings (#5844) @TChukwuleta @dennisreimann
|
||||
* UI: Improve Create First Store view (#5854) @dennisreimann
|
||||
* Receipts: Smaller printed receipts (#5856) @Kukks
|
||||
@ -51,6 +90,7 @@
|
||||
* Reports: Fix old payments not showing up in reports (#5812) @NicolasDorier
|
||||
* POS: Fix exception when asking for data with a top up item (#5816) @dennisreimann
|
||||
* Plugins: Do not have report name conflict with old plugin (#5826) @Kukks
|
||||
* Lightning: Do not throw when local node is not synced and using external ln node (#5859) @Kukks
|
||||
|
||||
## 1.12.5
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.101-bookworm-slim AS builder
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0.203-bookworm-slim AS builder
|
||||
ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||
WORKDIR /source
|
||||
COPY nuget.config nuget.config
|
||||
@ -21,7 +21,7 @@ ARG CONFIGURATION_NAME=Release
|
||||
ARG GIT_COMMIT
|
||||
RUN cd BTCPayServer && dotnet publish -p:GitCommit=${GIT_COMMIT} --output /app/ --configuration ${CONFIGURATION_NAME}
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0.1-bookworm-slim
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0.3-bookworm-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends iproute2 openssh-client \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
18
README.md
18
README.md
@ -213,13 +213,15 @@ BTCPay Server software, logo and designs are provided under [MIT License](https:
|
||||
|
||||
The BTCPay Server Project is proudly supported by these entities through the [BTCPay Server Foundation](https://foundation.btcpayserver.org/).
|
||||
|
||||
[](https://spiral.xyz)
|
||||
[](https://www.bailliegifford.com)
|
||||
[](https://strike.me)
|
||||
[](https://hrf.org)
|
||||
[](https://lunanode.com)
|
||||
[](https://walletofsatoshi.com/)
|
||||
[](https://coincards.com/)
|
||||
[](https://ivpn.net/)
|
||||
[](https://spiral.xyz)
|
||||
[](https://opensats.org)
|
||||
[](https://www.bailliegifford.com)
|
||||
[](https://tether.to)
|
||||
[](https://hrf.org)
|
||||
[](https://strike.me)
|
||||
[](https://lunanode.com)
|
||||
[](https://walletofsatoshi.com/)
|
||||
[](https://coincards.com/)
|
||||
[](https://ivpn.net/)
|
||||
|
||||
If you'd like to support the project, please visit the [donation page](https://btcpayserver.org/donate/).
|
||||
|
@ -1,24 +1,33 @@
|
||||
|
||||
# GreenField API Authorization Flow
|
||||
|
||||
The GreenField API allows two modes of authentication to its endpoints: Basic auth and API Keys.
|
||||
The GreenField API allows two modes of authentication to its endpoints: Basic Auth and API keys.
|
||||
|
||||
## Basic auth
|
||||
Basic auth allows you to seamlessly integrate with BTCPay Server's user system using only a traditional user/password login form. This is however a security risk if the application is a third party as they will receive your credentials in plain text and will be able to access your full account.
|
||||
|
||||
## API Keys
|
||||
BTCPay Server's GreenField API also allows users to generate API keys with [specific permissions](https://docs.btcpayserver.org/API/Greenfield/v1/#section/Authentication/API%20Key). **If you are integrating BTCPay Server into your third-party application, this is the recommended way.**
|
||||
BTCPay Server's Greenfield API also allows users to generate API keys with [specific permissions](https://docs.btcpayserver.org/API/Greenfield/v1/#section/Authentication/API_Key). **If you are integrating BTCPay Server into your third-party application, this is the recommended way.**
|
||||
|
||||
Asking a user to generate a dedicated API key, with a specific set of permissions can be a bad UX experience. For this scenario, we have the [Authorize User UI](https://docs.btcpayserver.org/API/Greenfield/v1/#tag/Authorization). This allows external applications to request the user to generate an API key with a specific set of permissions by simply generating a URL to BTCPay Server and redirecting the user to it.
|
||||
### Manually create an API key
|
||||
Users can create a new API key in the BTCPay Server UI under `Account` -> `Manage account` -> `API keys`
|
||||
|
||||
### Create API keys over the API itself
|
||||
|
||||
A user can create an API key for themselves using the [Create API Key endpoint](https://docs.btcpayserver.org/API/Greenfield/v1/#operation/APIKeys_CreateAPIKey) via Basic Auth or an unrestricted API key. Server administrators can create API keys for any user using the [Create API key for user endpoint](https://docs.btcpayserver.org/API/Greenfield/v1/#operation/ApiKeys_CreateUserApiKey).
|
||||
|
||||
### Interactive API key setup flow
|
||||
|
||||
Asking a user to generate a dedicated API key, with a specific set of permissions manually can be a bad UX experience. For this scenario, we have the [Authorize User UI](https://docs.btcpayserver.org/API/Greenfield/v1/#tag/Authorization). This allows external applications to request the user to generate an API key with a specific set of permissions by simply generating a URL to BTCPay Server and redirecting the user to it.
|
||||
Additionally, there are 2 optional parameters to the endpoint which allow a more seamless integration:
|
||||
* if `redirect` is specified, once the API key is created, BTCPay Server redirects the user via a POST submission to the specified `redirect` URL, with a json body containing the API key, user id, and permissions granted.
|
||||
* if `applicationIdentifier` is specified (along with `redirect`), BTCPay Server will check if there is an existing API key associated with the user that also has this application identifier, redirect host AND the permissions required match. `applicationIdentifier` is ignored if `redirect` is not specified.
|
||||
|
||||
Some examples of a generated Authorize URL:
|
||||
* `https://mainnet.demo.btcpayserver.org/api-keys/authorize` - A simplistic request, where no permission is requested. Useful to prove that a user exists on a specific BTCPay Server instance.
|
||||
* `https://mainnet.demo.btcpayserver.org/api-keys/authorize` - A simplistic request, where no permission is requested. Useful to prove that a user exists on a specific BTCPay Server instance.
|
||||
* `https://mainnet.demo.btcpayserver.org/api-keys/authorize?applicationName=Your%20Application` - Indicates that the API key is being generated for `Your Application`
|
||||
* `https://mainnet.demo.btcpayserver.org/api-keys/authorize?applicationName=Your%20Application&redirect=http://gozo.com` - Redirects the user via a POST to `http://gozo.com` with a JSON body containing the API key and its info.
|
||||
* `https://mainnet.demo.btcpayserver.org/api-keys/authorize?applicationName=Your%20Application&redirect=http://gozo.com&applicationIdentifier=gozo` - Attempts to match a previously created API key based on the app identifier, domain and permissions and is prompted.
|
||||
* `https://mainnet.demo.btcpayserver.org/api-keys/authorize?permissions=btcpay.store.cancreateinvoice&permissions=btcpay.store.canviewinvoices` - A request asking for permissions to create and view invoices on all stores available to the user
|
||||
* `https://mainnet.demo.btcpayserver.org/api-keys/authorize?permissions=btcpay.store.cancreateinvoice&permissions=btcpay.store.canviewinvoices&selectiveStores=true` - A request asking for permissions to create and view invoices on stores but also allows the user to choose which stores the application will have the permission to.
|
||||
* `https://mainnet.demo.btcpayserver.org/api-keys/authorize?permissions=btcpay.store.cancreateinvoice&permissions=btcpay.store.canviewinvoices&strict=false` - A request asking for permissions but allows the user to remove or add to the requested permission list.
|
||||
* `https://mainnet.demo.btcpayserver.org/api-keys/authorize?permissions=btcpay.store.cancreateinvoice&permissions=btcpay.store.canviewinvoices&strict=false` - A request asking for permissions but allows the user to remove or add to the requested permission list.
|
||||
|
Reference in New Issue
Block a user