Compare commits
90 Commits
v1.11.0-rc
...
v1.11.5
Author | SHA1 | Date | |
---|---|---|---|
66a064e78b | |||
c4f8c4c7b4 | |||
22bbafa659 | |||
8cdfaba20c | |||
7da82826fb | |||
9a46a64cad | |||
33198d693d | |||
eaeb7021d5 | |||
b443acd43b | |||
616883648f | |||
7873f94848 | |||
17d1832dad | |||
f034e2cd65 | |||
19de73f9da | |||
00acbccd7f | |||
77d8e202d3 | |||
44df8cf0c5 | |||
e694568674 | |||
163f805f3b | |||
492512f527 | |||
73a4ac599c | |||
4aedf76f1f | |||
2d38113c66 | |||
445e1b7bd9 | |||
019ac7ae31 | |||
2b3b025bd8 | |||
57bc90ad03 | |||
089e16020e | |||
0c4f31794d | |||
cdffe9b355 | |||
5a28cf9e87 | |||
3b05de7f30 | |||
79b2f1652b | |||
b32e0e7cce | |||
1f9fbbee22 | |||
8c9f325c9f | |||
9bf1e35bf4 | |||
32e830a1c5 | |||
561bae071f | |||
08b6942c59 | |||
4564f9a46c | |||
58a1c6d2c8 | |||
97acec340c | |||
52790a6954 | |||
af6249a741 | |||
17064ab3c8 | |||
1487bf4ff5 | |||
e8c0858558 | |||
56fa3fe8f2 | |||
583813883c | |||
c69f95bdce | |||
b3df403980 | |||
90ce75ee21 | |||
1c5fcfe094 | |||
45c1fb42ee | |||
64bd493996 | |||
ec6029409e | |||
c0fc31c69a | |||
b5d0188f21 | |||
0ccbaf4bd6 | |||
ed43fb2071 | |||
d67ebd957e | |||
19d360a543 | |||
7dc41ebcea | |||
1eb7c727f3 | |||
ede8171408 | |||
2538f3d8f6 | |||
ac64f5e395 | |||
1a7a731b54 | |||
86f4d48bcb | |||
83536bee88 | |||
abfd6ea1dc | |||
688e873f7a | |||
c88df08350 | |||
82586590a7 | |||
88c66f30f2 | |||
9132592717 | |||
c0ffab768a | |||
69190081c8 | |||
093206cf1e | |||
a0110b7570 | |||
6d65feca4c | |||
95be0242b6 | |||
79e121c3af | |||
676ac2fe46 | |||
8eabdab53a | |||
957fb09ffc | |||
4bffe117a9 | |||
05b01a13c8 | |||
08e21c1a5d |
@ -1,3 +1,4 @@
|
||||
using System.Web;
|
||||
using Ganss.XSS;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
@ -21,6 +22,11 @@ namespace BTCPayServer.Abstractions.Services
|
||||
{
|
||||
return _htmlHelper.Raw(_htmlSanitizer.Sanitize(value));
|
||||
}
|
||||
|
||||
public IHtmlContent RawEncode(string value)
|
||||
{
|
||||
return _htmlHelper.Raw(HttpUtility.HtmlEncode(_htmlSanitizer.Sanitize(value)));
|
||||
}
|
||||
|
||||
public IHtmlContent Json(object model)
|
||||
{
|
||||
|
@ -16,7 +16,7 @@
|
||||
<Platforms>AnyCPU</Platforms>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.2</Version>
|
||||
<Version Condition=" '$(Version)' == '' ">1.7.3</Version>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
@ -32,7 +32,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BTCPayServer.Lightning.Common" Version="1.3.21" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.24" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="icon.png" Pack="true" PackagePath="\" />
|
||||
|
@ -55,7 +55,7 @@ namespace BTCPayServer.Client
|
||||
}
|
||||
|
||||
public virtual async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
|
||||
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null,
|
||||
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null, int skip = 0,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var query = new Dictionary<string, object>();
|
||||
@ -67,6 +67,10 @@ namespace BTCPayServer.Client
|
||||
{
|
||||
query.Add(nameof(labelFilter), labelFilter);
|
||||
}
|
||||
if (skip != 0)
|
||||
{
|
||||
query.Add(nameof(skip), skip);
|
||||
}
|
||||
var response =
|
||||
await _httpClient.SendAsync(
|
||||
CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", query), token);
|
||||
|
@ -37,6 +37,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string RedirectUrl { get; set; } = null;
|
||||
public bool? RedirectAutomatically { get; set; } = null;
|
||||
public bool? RequiresRefundEmail { get; set; } = null;
|
||||
public bool? Archived { get; set; } = null;
|
||||
public string FormId { get; set; } = null;
|
||||
public string EmbeddedCSS { get; set; } = null;
|
||||
public CheckoutType? CheckoutType { get; set; } = null;
|
||||
@ -78,6 +79,7 @@ namespace BTCPayServer.Client.Models
|
||||
public bool? DisplayPerksValue { get; set; } = null;
|
||||
public bool? DisplayPerksRanking { get; set; } = null;
|
||||
public bool? SortPerksByPopularity { get; set; } = null;
|
||||
public bool? Archived { get; set; } = null;
|
||||
public string[] Sounds { get; set; } = null;
|
||||
public string[] AnimationColors { get; set; } = null;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ namespace BTCPayServer.Client.Models
|
||||
public class PaymentRequestData : PaymentRequestBaseData
|
||||
{
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public PaymentRequestData.PaymentRequestStatus Status { get; set; }
|
||||
public PaymentRequestStatus Status { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset CreatedTime { get; set; }
|
||||
public string Id { get; set; }
|
||||
@ -16,7 +16,8 @@ namespace BTCPayServer.Client.Models
|
||||
{
|
||||
Pending = 0,
|
||||
Completed = 1,
|
||||
Expired = 2
|
||||
Expired = 2,
|
||||
Processing = 3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ namespace BTCPayServer.Client.Models
|
||||
public string AppType { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? Archived { get; set; }
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
|
||||
public DateTimeOffset Created { get; set; }
|
||||
}
|
||||
|
@ -45,6 +45,9 @@ namespace BTCPayServer.Client.Models
|
||||
public bool LazyPaymentMethods { get; set; }
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool Archived { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool ShowRecommendedFee { get; set; } = true;
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
|
@ -33,6 +33,7 @@ namespace BTCPayServer.Client
|
||||
public const string CanManageUsers = "btcpay.server.canmanageusers";
|
||||
public const string CanDeleteUser = "btcpay.user.candeleteuser";
|
||||
public const string CanManagePullPayments = "btcpay.store.canmanagepullpayments";
|
||||
public const string CanArchivePullPayments = "btcpay.store.canarchivepullpayments";
|
||||
public const string CanCreatePullPayments = "btcpay.store.cancreatepullpayments";
|
||||
public const string CanCreateNonApprovedPullPayments = "btcpay.store.cancreatenonapprovedpullpayments";
|
||||
public const string CanViewCustodianAccounts = "btcpay.store.canviewcustodianaccounts";
|
||||
@ -69,6 +70,7 @@ namespace BTCPayServer.Client
|
||||
yield return CanViewLightningInvoiceInStore;
|
||||
yield return CanCreateLightningInvoiceInStore;
|
||||
yield return CanManagePullPayments;
|
||||
yield return CanArchivePullPayments;
|
||||
yield return CanCreatePullPayments;
|
||||
yield return CanCreateNonApprovedPullPayments;
|
||||
yield return CanViewCustodianAccounts;
|
||||
@ -253,7 +255,7 @@ namespace BTCPayServer.Client
|
||||
Policies.CanUseLightningNodeInStore);
|
||||
|
||||
PolicyHasChild(policyMap,Policies.CanManageUsers, Policies.CanCreateUser);
|
||||
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments);
|
||||
PolicyHasChild(policyMap,Policies.CanManagePullPayments, Policies.CanCreatePullPayments, Policies.CanArchivePullPayments);
|
||||
PolicyHasChild(policyMap,Policies.CanCreatePullPayments, Policies.CanCreateNonApprovedPullPayments);
|
||||
PolicyHasChild(policyMap,Policies.CanModifyPaymentRequests, Policies.CanViewPaymentRequests);
|
||||
PolicyHasChild(policyMap,Policies.CanModifyProfile, Policies.CanViewProfile);
|
||||
|
@ -16,7 +16,7 @@ namespace BTCPayServer
|
||||
DefaultRateRules = new[]
|
||||
{
|
||||
"BTG_X = BTG_BTC * BTC_X",
|
||||
"BTG_BTC = exmo(BTG_BTC)",
|
||||
"BTG_BTC = gate(BTG_BTC)",
|
||||
},
|
||||
CryptoImagePath = "imlegacy/btg.svg",
|
||||
LightningImagePath = "imlegacy/btg-lightning.svg",
|
||||
|
@ -14,6 +14,7 @@ namespace BTCPayServer.Data
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public bool TagAllInvoices { get; set; }
|
||||
public string Settings { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
@ -48,6 +48,7 @@ namespace BTCPayServer.Data
|
||||
public IEnumerable<StoreSettingData> Settings { get; set; }
|
||||
public IEnumerable<FormData> Forms { get; set; }
|
||||
public IEnumerable<StoreRole> StoreRoles { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
||||
internal static void OnModelCreating(ModelBuilder builder, DatabaseFacade databaseFacade)
|
||||
{
|
||||
|
@ -0,0 +1,39 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20230906135844_AddArchivedFlagForStoresAndApps")]
|
||||
public partial class AddArchivedFlagForStoresAndApps : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Archived",
|
||||
table: "Stores",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Archived",
|
||||
table: "Apps",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Archived",
|
||||
table: "Stores");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Archived",
|
||||
table: "Apps");
|
||||
}
|
||||
}
|
||||
}
|
@ -79,6 +79,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("AppType")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Archived")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset>("Created")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@ -751,6 +754,9 @@ namespace BTCPayServer.Migrations
|
||||
b.Property<string>("Id")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("Archived")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DefaultCrypto")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9" />
|
||||
<PackageReference Include="NBitcoin" Version="7.0.24" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="DigitalRuby.ExchangeSharp" Version="1.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -663,7 +663,7 @@ donation:
|
||||
Assert.Equal(3, vmview.Items.Length);
|
||||
Assert.Equal("good apple", vmview.Items[0].Title);
|
||||
Assert.Equal("orange", vmview.Items[1].Title);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price.Value);
|
||||
Assert.Equal(10.0m, vmview.Items[1].Price);
|
||||
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
||||
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
||||
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||
@ -680,7 +680,7 @@ donation:
|
||||
Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
invoices = await user.BitPay.GetInvoicesAsync();
|
||||
var appleInvoice = invoices.SingleOrDefault(invoice => invoice.ItemCode.Equals("apple"));
|
||||
Assert.NotNull(appleInvoice);
|
||||
Assert.Equal("good apple", appleInvoice.ItemDesc);
|
||||
@ -689,7 +689,7 @@ donation:
|
||||
var action = Assert.IsType<RedirectToActionResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 6.6m, choiceKey: "donation").Result);
|
||||
Assert.Equal(nameof(UIInvoiceController.Checkout), action.ActionName);
|
||||
invoices = user.BitPay.GetInvoices();
|
||||
invoices = await user.BitPay.GetInvoicesAsync();
|
||||
var donationInvoice = invoices.Single(i => i.Price == 6.6m);
|
||||
Assert.NotNull(donationInvoice);
|
||||
Assert.Equal("CAD", donationInvoice.Currency);
|
||||
|
@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.14" />
|
||||
<PackageReference Include="Selenium.Support" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver" Version="4.1.1" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="114.0.5735.9000" />
|
||||
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="116.0.5845.9600" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -54,13 +54,33 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps2.ListApps(user2.StoreId).Result).Model);
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Empty(appList2.Apps);
|
||||
Assert.Equal("test", appList.Apps[0].AppName);
|
||||
Assert.Equal(apps.CreatedAppId, appList.Apps[0].Id);
|
||||
Assert.True(appList.Apps[0].Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, appList.Apps[0].StoreId));
|
||||
Assert.Equal(user.StoreId, appList.Apps[0].StoreId);
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(appList.Apps[0].Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(appList.Apps[0].Id));
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(appList.Apps[0].Id).Result);
|
||||
Assert.Equal("test", app.AppName);
|
||||
Assert.Equal(apps.CreatedAppId, app.Id);
|
||||
Assert.True(app.Role.ToPermissionSet(app.StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||
Assert.Equal(user.StoreId, app.StoreId);
|
||||
// Archive
|
||||
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
Assert.True(app.Archived);
|
||||
Assert.IsType<NotFoundResult>(await crowdfund.ViewCrowdfund(app.Id));
|
||||
// Unarchive
|
||||
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||
Assert.EndsWith("/settings/crowdfund", redirect.Url);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
Assert.False(app.Archived);
|
||||
var crowdfundViewModel = await crowdfund.UpdateCrowdfund(app.Id).AssertViewModelAsync<UpdateCrowdfundViewModel>();
|
||||
crowdfundViewModel.Enabled = true;
|
||||
Assert.IsType<RedirectToActionResult>(crowdfund.UpdateCrowdfund(app.Id, crowdfundViewModel, "save").Result);
|
||||
Assert.IsType<ViewResult>(await crowdfund.ViewCrowdfund(app.Id));
|
||||
// Delete
|
||||
Assert.IsType<NotFoundResult>(apps2.DeleteApp(app.Id));
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
|
||||
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
||||
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
||||
Assert.Empty(appList.Apps);
|
||||
|
@ -23,6 +23,7 @@ using BTCPayServer.Payments.Bitcoin;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Labels;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -346,6 +347,65 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(Torrc.TryParse(input, out torrc));
|
||||
Assert.Equal(expected, torrc.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanParseCartItems()
|
||||
{
|
||||
Assert.True(AppService.TryParsePosCartItems(new JObject()
|
||||
{
|
||||
{"cart", new JArray()
|
||||
{
|
||||
new JObject()
|
||||
{
|
||||
{ "id", "ddd"},
|
||||
{"price", 4},
|
||||
{"count", 1}
|
||||
}
|
||||
}}
|
||||
}, out var items));
|
||||
Assert.Equal("ddd", items[0].Id);
|
||||
Assert.Equal(1, items[0].Count);
|
||||
Assert.Equal(4, items[0].Price);
|
||||
|
||||
// Using legacy parsing
|
||||
Assert.True(AppService.TryParsePosCartItems(new JObject()
|
||||
{
|
||||
{"cart", new JArray()
|
||||
{
|
||||
new JObject()
|
||||
{
|
||||
{ "id", "ddd"},
|
||||
{"price", new JObject()
|
||||
{
|
||||
{ "value", 8.49m }
|
||||
}
|
||||
},
|
||||
{"count", 1}
|
||||
}
|
||||
}}
|
||||
}, out items));
|
||||
Assert.Equal("ddd", items[0].Id);
|
||||
Assert.Equal(1, items[0].Count);
|
||||
Assert.Equal(8.49m, items[0].Price);
|
||||
|
||||
Assert.False(AppService.TryParsePosCartItems(new JObject()
|
||||
{
|
||||
{"cart", new JArray()
|
||||
{
|
||||
new JObject()
|
||||
{
|
||||
{ "id", "ddd"},
|
||||
{"price", new JObject()
|
||||
{
|
||||
{ "value", "nocrahs" }
|
||||
}
|
||||
},
|
||||
{"count", 1}
|
||||
}
|
||||
}}
|
||||
}, out items));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanCalculateDust()
|
||||
{
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
@ -16,8 +15,6 @@ using BTCPayServer.Models.InvoicingModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.PayoutProcessors;
|
||||
using BTCPayServer.PayoutProcessors.OnChain;
|
||||
using BTCPayServer.Plugins;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Custodian.Client.MockCustodian;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
@ -25,7 +22,6 @@ using BTCPayServer.Services.Notifications.Blobs;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
@ -301,6 +297,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(user.StoreId, app.StoreId);
|
||||
Assert.Equal("PointOfSale", app.AppType);
|
||||
Assert.Equal("test app title", app.Title);
|
||||
Assert.False(app.Archived);
|
||||
|
||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
@ -324,17 +321,20 @@ namespace BTCPayServer.Tests
|
||||
new CreatePointOfSaleAppRequest()
|
||||
{
|
||||
AppName = "new app name",
|
||||
Title = "new app title"
|
||||
Title = "new app title",
|
||||
Archived = true
|
||||
}
|
||||
);
|
||||
// Test generic GET app endpoint first
|
||||
retrievedApp = await client.GetApp(app.Id);
|
||||
Assert.Equal("new app name", retrievedApp.Name);
|
||||
Assert.True(retrievedApp.Archived);
|
||||
|
||||
// Test the POS-specific endpoint also
|
||||
var retrievedPosApp = await client.GetPosApp(app.Id);
|
||||
Assert.Equal("new app name", retrievedPosApp.Name);
|
||||
Assert.Equal("new app title", retrievedPosApp.Title);
|
||||
Assert.True(retrievedPosApp.Archived);
|
||||
|
||||
// Make sure we return a 404 if we try to delete an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
@ -466,6 +466,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal("test app from API", app.Name);
|
||||
Assert.Equal(user.StoreId, app.StoreId);
|
||||
Assert.Equal("Crowdfund", app.AppType);
|
||||
Assert.False(app.Archived);
|
||||
|
||||
// Make sure we return a 404 if we try to get an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
@ -482,11 +483,13 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(app.Name, retrievedApp.Name);
|
||||
Assert.Equal(app.StoreId, retrievedApp.StoreId);
|
||||
Assert.Equal(app.AppType, retrievedApp.AppType);
|
||||
Assert.False(retrievedApp.Archived);
|
||||
|
||||
// Test the crowdfund-specific endpoint also
|
||||
var retrievedPosApp = await client.GetCrowdfundApp(app.Id);
|
||||
Assert.Equal(app.Name, retrievedPosApp.Name);
|
||||
Assert.Equal(app.Title, retrievedPosApp.Title);
|
||||
var retrievedCfApp = await client.GetCrowdfundApp(app.Id);
|
||||
Assert.Equal(app.Name, retrievedCfApp.Name);
|
||||
Assert.Equal(app.Title, retrievedCfApp.Title);
|
||||
Assert.False(retrievedCfApp.Archived);
|
||||
|
||||
// Make sure we return a 404 if we try to delete an app that doesn't exist
|
||||
await AssertHttpError(404, async () =>
|
||||
@ -536,10 +539,12 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(posApp.Name, apps[0].Name);
|
||||
Assert.Equal(posApp.StoreId, apps[0].StoreId);
|
||||
Assert.Equal(posApp.AppType, apps[0].AppType);
|
||||
Assert.False(apps[0].Archived);
|
||||
|
||||
Assert.Equal(crowdfundApp.Name, apps[1].Name);
|
||||
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
|
||||
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
|
||||
Assert.False(apps[1].Archived);
|
||||
|
||||
// Get all apps for all store now
|
||||
apps = await client.GetAllApps();
|
||||
@ -549,15 +554,17 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(posApp.Name, apps[0].Name);
|
||||
Assert.Equal(posApp.StoreId, apps[0].StoreId);
|
||||
Assert.Equal(posApp.AppType, apps[0].AppType);
|
||||
Assert.False(apps[0].Archived);
|
||||
|
||||
Assert.Equal(crowdfundApp.Name, apps[1].Name);
|
||||
Assert.Equal(crowdfundApp.StoreId, apps[1].StoreId);
|
||||
Assert.Equal(crowdfundApp.AppType, apps[1].AppType);
|
||||
Assert.False(apps[1].Archived);
|
||||
|
||||
Assert.Equal(newApp.Name, apps[2].Name);
|
||||
Assert.Equal(newApp.StoreId, apps[2].StoreId);
|
||||
Assert.Equal(newApp.AppType, apps[2].AppType);
|
||||
|
||||
Assert.False(apps[2].Archived);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -878,7 +885,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
TestLogs.LogInformation("Can't archive without knowing the walletId");
|
||||
var ex = await AssertAPIError("missing-permission", async () => await client.ArchivePullPayment("lol", result.Id));
|
||||
Assert.Equal("btcpay.store.canmanagepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
|
||||
Assert.Equal("btcpay.store.canarchivepullpayments", ((GreenfieldPermissionAPIError)ex.APIError).MissingPermission);
|
||||
TestLogs.LogInformation("Can't archive without permission");
|
||||
await AssertAPIError("unauthenticated", async () => await unauthenticated.ArchivePullPayment(storeId, result.Id));
|
||||
await client.ArchivePullPayment(storeId, result.Id);
|
||||
@ -1272,7 +1279,7 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
await user.MakeAdmin();
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
|
||||
@ -1351,6 +1358,13 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
tester.DeleteStore = false;
|
||||
Assert.Empty(await client.GetStores());
|
||||
|
||||
// Archive
|
||||
var archivableStore = await client.CreateStore(new CreateStoreRequest { Name = "Archivable" });
|
||||
Assert.False(archivableStore.Archived);
|
||||
archivableStore = await client.UpdateStore(archivableStore.Id, new UpdateStoreRequest { Name = "Archived", Archived = true });
|
||||
Assert.Equal("Archived", archivableStore.Name);
|
||||
Assert.True(archivableStore.Archived);
|
||||
}
|
||||
|
||||
private async Task<GreenfieldValidationException> AssertValidationError(string[] fields, Func<Task> act)
|
||||
@ -1592,7 +1606,7 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
await user.MakeAdmin();
|
||||
var client = await user.CreateClient(Policies.Unrestricted);
|
||||
var viewOnly = await user.CreateClient(Policies.CanViewPaymentRequests);
|
||||
@ -1674,11 +1688,18 @@ namespace BTCPayServer.Tests
|
||||
BitcoinAddress.Create(invoice.BitcoinAddress, tester.ExplorerNode.Network), invoice.BtcDue);
|
||||
});
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_PAID, user.BitPay.GetInvoice(invoiceId).Status);
|
||||
if (!partialPayment)
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_PAID, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||
if (!partialPayment)
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Processing, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
await tester.ExplorerNode.GenerateAsync(1);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
Assert.Equal(Invoice.STATUS_CONFIRMED, (await user.BitPay.GetInvoiceAsync(invoiceId)).Status);
|
||||
if (!partialPayment)
|
||||
Assert.Equal(PaymentRequestData.PaymentRequestStatus.Completed, (await client.GetPaymentRequest(user.StoreId, paymentTestPaymentRequest.Id)).Status);
|
||||
});
|
||||
}
|
||||
await Pay(invoiceId);
|
||||
|
||||
@ -3212,6 +3233,9 @@ namespace BTCPayServer.Tests
|
||||
});
|
||||
var transaction = await client.GetOnChainWalletTransaction(walletId.StoreId, walletId.CryptoCode, txdata.TransactionHash.ToString());
|
||||
|
||||
// Check skip doesn't crash
|
||||
await client.ShowOnChainWalletTransactions(walletId.StoreId, walletId.CryptoCode, skip: 1);
|
||||
|
||||
Assert.Equal(transaction.TransactionHash, txdata.TransactionHash);
|
||||
Assert.Equal(String.Empty, transaction.Comment);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
|
@ -1,8 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Controllers;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
@ -123,12 +125,14 @@ donation:
|
||||
price: 1.02
|
||||
custom: true
|
||||
";
|
||||
vmpos.Currency = "EUR";
|
||||
vmpos.Template = AppService.SerializeTemplate(MigrationStartupTask.ParsePOSYML(vmpos.Template));
|
||||
Assert.IsType<RedirectToActionResult>(pos.UpdatePointOfSale(app.Id, vmpos).Result);
|
||||
await pos.UpdatePointOfSale(app.Id).AssertViewModelAsync<UpdatePointOfSaleViewModel>();
|
||||
var publicApps = user.GetController<UIPointOfSaleController>();
|
||||
var vmview = await publicApps.ViewPointOfSale(app.Id, PosViewType.Cart).AssertViewModelAsync<ViewPointOfSaleViewModel>();
|
||||
|
||||
Assert.Equal("EUR", vmview.CurrencyCode);
|
||||
// apple shouldn't be available since we it's set to "disabled: true" above
|
||||
Assert.Equal(2, vmview.Items.Length);
|
||||
Assert.Equal("orange", vmview.Items[0].Title);
|
||||
@ -139,6 +143,41 @@ donation:
|
||||
// apple is not found
|
||||
Assert.IsType<NotFoundResult>(publicApps
|
||||
.ViewPointOfSale(app.Id, PosViewType.Cart, 0, choiceKey: "apple").Result);
|
||||
|
||||
// List
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
apps = user.GetController<UIAppsController>();
|
||||
appData = new AppData { Id = app.Id, StoreDataId = app.StoreId, Name = app.AppName, AppType = appType, Settings = "{\"currency\":\"EUR\"}" };
|
||||
apps.HttpContext.SetAppData(appData);
|
||||
pos.HttpContext.SetAppData(appData);
|
||||
Assert.Single(appList.Apps);
|
||||
Assert.Equal("test", app.AppName);
|
||||
Assert.True(app.Role.ToPermissionSet(appList.Apps[0].StoreId).Contains(Policies.CanModifyStoreSettings, app.StoreId));
|
||||
Assert.Equal(user.StoreId, app.StoreId);
|
||||
Assert.False(app.Archived);
|
||||
// Archive
|
||||
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
Assert.Empty(appList.Apps);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId, archived: true).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
Assert.True(app.Archived);
|
||||
Assert.IsType<NotFoundResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
|
||||
// Unarchive
|
||||
redirect = Assert.IsType<RedirectResult>(apps.ToggleArchive(app.Id).Result);
|
||||
Assert.EndsWith("/settings/pos", redirect.Url);
|
||||
appList = Assert.IsType<ListAppsViewModel>(Assert.IsType<ViewResult>(apps.ListApps(user.StoreId).Result).Model);
|
||||
app = appList.Apps[0];
|
||||
Assert.False(app.Archived);
|
||||
Assert.IsType<ViewResult>(await publicApps.ViewPointOfSale(app.Id, PosViewType.Static));
|
||||
// Delete
|
||||
Assert.IsType<ViewResult>(apps.DeleteApp(app.Id));
|
||||
var redirectToAction = Assert.IsType<RedirectToActionResult>(apps.DeleteAppPost(app.Id).Result);
|
||||
Assert.Equal(nameof(UIStoresController.Dashboard), redirectToAction.ActionName);
|
||||
appList = await apps.ListApps(user.StoreId).AssertViewModelAsync<ListAppsViewModel>();
|
||||
Assert.Empty(appList.Apps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ namespace BTCPayServer.Tests
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var user2 = tester.NewAccount();
|
||||
|
||||
await user2.GrantAccessAsync();
|
||||
|
||||
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
||||
@ -162,7 +161,7 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
|
||||
var paymentRequestController = user.GetController<UIPaymentRequestController>();
|
||||
@ -170,7 +169,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.IsType<NotFoundResult>(await
|
||||
paymentRequestController.CancelUnpaidPendingInvoice(Guid.NewGuid().ToString(), false));
|
||||
|
||||
var request = new UpdatePaymentRequestViewModel()
|
||||
var request = new UpdatePaymentRequestViewModel
|
||||
{
|
||||
Title = "original juice",
|
||||
Currency = "BTC",
|
||||
|
@ -565,7 +565,7 @@ namespace BTCPayServer.Tests
|
||||
walletId ??= WalletId;
|
||||
GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
Driver.FindElement(By.Id("generateButton")).Click();
|
||||
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("value");
|
||||
var addressStr = Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||
for (var i = 0; i < coins; i++)
|
||||
{
|
||||
|
@ -38,6 +38,7 @@ using OpenQA.Selenium.Support.Extensions;
|
||||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -809,6 +810,27 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
|
||||
s.GoToUrl(storeUrl);
|
||||
Assert.Contains("ReturnUrl", s.Driver.Url);
|
||||
|
||||
// Archive store
|
||||
(storeName, storeId) = s.CreateNewStore();
|
||||
|
||||
s.Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
|
||||
Assert.Contains(storeName, s.Driver.FindElement(By.Id("StoreSelectorMenu")).Text);
|
||||
s.Driver.FindElement(By.Id($"StoreSelectorMenuItem-{storeId}")).Click();
|
||||
s.GoToStore();
|
||||
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
|
||||
Assert.Contains("The store has been archived and will no longer appear in the stores list by default.", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("StoreSelectorToggle")).Click();
|
||||
Assert.DoesNotContain(storeName, s.Driver.FindElement(By.Id("StoreSelectorMenu")).Text);
|
||||
Assert.Contains("1 Archived Store", s.Driver.FindElement(By.Id("StoreSelectorMenu")).Text);
|
||||
s.Driver.FindElement(By.Id("StoreSelectorArchived")).Click();
|
||||
|
||||
var storeLink = s.Driver.FindElement(By.Id($"Store-{storeId}"));
|
||||
Assert.Contains(storeName, storeLink.Text);
|
||||
storeLink.Click();
|
||||
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
|
||||
Assert.Contains("The store has been unarchived and will appear in the stores list by default again.", s.FindAlertMessage().Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -905,7 +927,8 @@ namespace BTCPayServer.Tests
|
||||
Policies.CanModifyStoreSettings,
|
||||
Policies.CanCreateNonApprovedPullPayments,
|
||||
Policies.CanCreatePullPayments,
|
||||
Policies.CanManagePullPayments
|
||||
Policies.CanManagePullPayments,
|
||||
Policies.CanArchivePullPayments,
|
||||
});
|
||||
AssertPermissions(pageSource, false,
|
||||
new[]
|
||||
@ -978,6 +1001,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
var appId = s.Driver.Url.Split('/')[4];
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
@ -988,14 +1012,14 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(s.Driver.PageSource.Contains("Tea shop"), "Unable to create PoS");
|
||||
Assert.True(s.Driver.PageSource.Contains("Cart"), "PoS not showing correct default view");
|
||||
Assert.True(s.Driver.PageSource.Contains("Take my money"), "PoS not showing correct default view");
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".posItem:not(.d-none)")).Count);
|
||||
Assert.Equal(6, s.Driver.FindElements(By.CssSelector(".posItem.posItem--displayed")).Count);
|
||||
|
||||
var drinks = s.Driver.FindElement(By.CssSelector("label[for='Category-Drinks']"));
|
||||
Assert.Equal("Drinks", drinks.Text);
|
||||
drinks.Click();
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector(".posItem:not(.d-none)")));
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector(".posItem.posItem--displayed")));
|
||||
s.Driver.FindElement(By.CssSelector("label[for='Category-*']")).Click();
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector(".posItem:not(.d-none)")).Count);
|
||||
Assert.Equal(6, s.Driver.FindElements(By.CssSelector(".posItem.posItem--displayed")).Count);
|
||||
|
||||
s.Driver.Url = posBaseUrl + "/static";
|
||||
Assert.False(s.Driver.PageSource.Contains("Cart"), "Static PoS not showing correct view");
|
||||
@ -1046,6 +1070,24 @@ namespace BTCPayServer.Tests
|
||||
// We are only if explicitly going to /
|
||||
s.GoToUrl("/");
|
||||
Assert.Contains("Tea shop", s.Driver.PageSource);
|
||||
|
||||
// Archive
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("Nav-ArchivedApps")));
|
||||
s.Driver.SwitchTo().Window(windows[0]);
|
||||
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
|
||||
Assert.Contains("The app has been archived and will no longer appear in the apps list by default.", s.FindAlertMessage().Text);
|
||||
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ViewApp")));
|
||||
Assert.Contains("1 Archived App", s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Text);
|
||||
s.Driver.Navigate().GoToUrl(posBaseUrl);
|
||||
Assert.Contains("Page not found", s.Driver.Title, StringComparison.OrdinalIgnoreCase);
|
||||
s.Driver.Navigate().Back();
|
||||
s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Click();
|
||||
|
||||
// Unarchive
|
||||
s.Driver.FindElement(By.Id($"App-{appId}")).Click();
|
||||
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
|
||||
Assert.Contains("The app has been unarchived and will appear in the apps list by default again.", s.FindAlertMessage().Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1079,17 +1121,37 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
var appId = s.Driver.Url.Split('/')[4];
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
var windows = s.Driver.WindowHandles;
|
||||
Assert.Equal(2, windows.Count);
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
var cfUrl = s.Driver.Url;
|
||||
|
||||
Assert.Equal("Currently active!",
|
||||
s.Driver.FindElement(By.CssSelector("[data-test='time-state']")).Text);
|
||||
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(windows[0]);
|
||||
|
||||
// Archive
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("Nav-ArchivedApps")));
|
||||
s.Driver.SwitchTo().Window(windows[0]);
|
||||
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
|
||||
Assert.Contains("The app has been archived and will no longer appear in the apps list by default.", s.FindAlertMessage().Text);
|
||||
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("ViewApp")));
|
||||
Assert.Contains("1 Archived App", s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Text);
|
||||
s.Driver.Navigate().GoToUrl(cfUrl);
|
||||
Assert.Contains("Page not found", s.Driver.Title, StringComparison.OrdinalIgnoreCase);
|
||||
s.Driver.Navigate().Back();
|
||||
s.Driver.FindElement(By.Id("Nav-ArchivedApps")).Click();
|
||||
|
||||
// Unarchive
|
||||
s.Driver.FindElement(By.Id($"App-{appId}")).Click();
|
||||
s.Driver.FindElement(By.Id("btn-archive-toggle")).Click();
|
||||
Assert.Contains("The app has been unarchived and will appear in the apps list by default again.", s.FindAlertMessage().Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1099,13 +1161,13 @@ namespace BTCPayServer.Tests
|
||||
await s.StartAsync();
|
||||
s.RegisterNewUser();
|
||||
s.CreateNewStore();
|
||||
s.EnableCheckout(CheckoutType.V1);
|
||||
s.AddDerivationScheme();
|
||||
|
||||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys(".01");
|
||||
|
||||
var currencyInput = s.Driver.FindElement(By.Id("Currency"));
|
||||
Assert.Equal("USD", currencyInput.GetAttribute("value"));
|
||||
@ -1148,9 +1210,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
// test invoice creation, click with JS, because the button is inside a sticky header
|
||||
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
|
||||
// checkout v1
|
||||
s.Driver.WaitForElement(By.CssSelector("invoice"));
|
||||
Assert.Contains("Awaiting Payment", s.Driver.PageSource);
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
|
||||
// amount and currency should not be editable, because invoice exists
|
||||
s.GoToUrl(editUrl);
|
||||
@ -1171,6 +1231,36 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.WaitForElement(By.Id($"ToggleArchival-{payReqId}")).Click();
|
||||
Assert.Contains("The payment request has been unarchived", s.FindAlertMessage().Text);
|
||||
Assert.Contains("Pay123", s.Driver.PageSource);
|
||||
|
||||
// payment
|
||||
s.GoToUrl(viewUrl);
|
||||
s.Driver.ExecuteJavaScript("document.querySelector('[data-test=\"pay-button\"]').click()");
|
||||
|
||||
// Pay full amount
|
||||
s.PayInvoice();
|
||||
|
||||
// Processing
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var processingSection = s.Driver.WaitForElement(By.Id("processing"));
|
||||
Assert.True(processingSection.Displayed);
|
||||
Assert.Contains("Payment Received", processingSection.Text);
|
||||
Assert.Contains("Your payment has been received and is now processing", processingSection.Text);
|
||||
});
|
||||
|
||||
s.GoToUrl(viewUrl);
|
||||
Assert.Equal("Processing", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
|
||||
s.Driver.Navigate().Back();
|
||||
|
||||
// Mine
|
||||
s.MineBlockOnInvoiceCheckout();
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.Contains("Mined 1 block",
|
||||
s.Driver.WaitForElement(By.Id("CheatSuccessMessage")).Text);
|
||||
});
|
||||
s.GoToUrl(viewUrl);
|
||||
Assert.Equal("Settled", s.Driver.WaitForElement(By.CssSelector("[data-test='status']")).Text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
@ -1184,7 +1274,7 @@ namespace BTCPayServer.Tests
|
||||
var walletId = new WalletId(storeId, "BTC");
|
||||
s.GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
s.Driver.FindElement(By.Id("generateButton")).Click();
|
||||
var addressStr = s.Driver.FindElement(By.Id("Address")).GetAttribute("value");
|
||||
var addressStr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
var address = BitcoinAddress.Create(addressStr,
|
||||
((BTCPayNetwork)s.Server.NetworkProvider.GetNetwork("BTC")).NBitcoinNetwork);
|
||||
await s.Server.ExplorerNode.GenerateAsync(1);
|
||||
@ -1400,7 +1490,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
// no previous page in the wizard, hence no back button
|
||||
Assert.True(s.Driver.ElementDoesNotExist(By.Id("GoBack")));
|
||||
var receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("value");
|
||||
var receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
|
||||
// Can add a label?
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
@ -1423,7 +1513,7 @@ namespace BTCPayServer.Tests
|
||||
//generate it again, should be the same one as before as nothing got used in the meantime
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.True(s.Driver.FindElement(By.CssSelector("#address-tab .qr-container")).Displayed);
|
||||
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("value"));
|
||||
Assert.Equal(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
Assert.Contains("test-label", s.Driver.PageSource);
|
||||
@ -1454,8 +1544,8 @@ namespace BTCPayServer.Tests
|
||||
await Task.Delay(200);
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("value"));
|
||||
receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("value");
|
||||
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
|
||||
receiveAddr = s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text");
|
||||
s.Driver.FindElement(By.Id("CancelWizard")).Click();
|
||||
|
||||
// Check the label is applied to the tx
|
||||
@ -1466,7 +1556,7 @@ namespace BTCPayServer.Tests
|
||||
s.GenerateWallet(cryptoCode, "", true);
|
||||
s.GoToWallet(null, WalletsNavPages.Receive);
|
||||
s.Driver.FindElement(By.CssSelector("button[value=generate-new-address]")).Click();
|
||||
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("value"));
|
||||
Assert.NotEqual(receiveAddr, s.Driver.FindElement(By.Id("Address")).GetAttribute("data-text"));
|
||||
|
||||
var invoiceId = s.CreateInvoice(storeId);
|
||||
var invoice = await s.Server.PayTester.InvoiceRepository.GetInvoice(invoiceId);
|
||||
@ -1760,6 +1850,7 @@ namespace BTCPayServer.Tests
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
s.Driver.WaitWalletTransactionsLoaded();
|
||||
Assert.Contains("transaction-label", s.Driver.PageSource);
|
||||
var labels = s.Driver.FindElements(By.CssSelector("#WalletTransactionsList tr:first-child div.transaction-label"));
|
||||
Assert.Equal(2, labels.Count);
|
||||
@ -1802,7 +1893,7 @@ namespace BTCPayServer.Tests
|
||||
s.GoToHome();
|
||||
//offline/external payout test
|
||||
s.Driver.FindElement(By.Id("NotificationsHandle")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#notificationsForm button")).Click();
|
||||
s.Driver.FindElement(By.Id("NotificationsMarkAllAsSeen")).Click();
|
||||
|
||||
var newStore = s.CreateNewStore();
|
||||
s.GenerateWallet("BTC", "", true, true);
|
||||
@ -1929,10 +2020,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(bolt, s.Driver.PageSource);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//auto-approve pull payments
|
||||
|
||||
s.GoToStore(StoreNavPages.PullPayments);
|
||||
s.Driver.FindElement(By.Id("NewPullPayment")).Click();
|
||||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
@ -1962,6 +2050,8 @@ namespace BTCPayServer.Tests
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Success);
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.FindElement(By.CssSelector("#lnurlwithdraw-button")).Click();
|
||||
s.Driver.WaitForElement(By.Id("qr-code-data-input"));
|
||||
|
||||
var lnurl = new Uri(LNURL.LNURL.Parse(s.Driver.FindElement(By.Id("qr-code-data-input")).GetAttribute("value"), out _).ToString().Replace("https", "http"));
|
||||
s.Driver.FindElement(By.CssSelector("button[data-bs-dismiss='modal']")).Click();
|
||||
var info = Assert.IsType<LNURLWithdrawRequest>(await LNURL.LNURL.FetchInformation(lnurl, s.Server.PayTester.HttpClient));
|
||||
@ -1975,7 +2065,7 @@ namespace BTCPayServer.Tests
|
||||
new LightMoney(0.0000001m, LightMoneyUnit.BTC),
|
||||
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None));
|
||||
var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
|
||||
var response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
@ -2010,7 +2100,7 @@ namespace BTCPayServer.Tests
|
||||
new LightMoney(0.0000001m, LightMoneyUnit.BTC),
|
||||
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None));
|
||||
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
|
||||
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
@ -2045,7 +2135,7 @@ namespace BTCPayServer.Tests
|
||||
amount,
|
||||
$"LNurl w payout test {DateTime.UtcNow.Ticks}",
|
||||
TimeSpan.FromHours(1), CancellationToken.None));
|
||||
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient);
|
||||
response = await info.SendRequest(bolt2.BOLT11, s.Server.PayTester.HttpClient, null,null);
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
s.Driver.Navigate().Refresh();
|
||||
@ -2064,7 +2154,6 @@ namespace BTCPayServer.Tests
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
@ -2101,7 +2190,6 @@ namespace BTCPayServer.Tests
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
@ -2176,7 +2264,6 @@ namespace BTCPayServer.Tests
|
||||
using var s = CreateSeleniumTester();
|
||||
s.Server.ActivateLightning();
|
||||
await s.StartAsync();
|
||||
|
||||
await s.Server.EnsureChannelsSetup();
|
||||
|
||||
s.RegisterNewUser(true);
|
||||
@ -2199,6 +2286,7 @@ namespace BTCPayServer.Tests
|
||||
s.Driver.SwitchTo().Window(windows[1]);
|
||||
s.Driver.WaitForElement(By.Id("PosItems"));
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
|
||||
var posUrl = s.Driver.Url;
|
||||
|
||||
// Select and clear
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click();
|
||||
@ -2207,34 +2295,81 @@ namespace BTCPayServer.Tests
|
||||
Thread.Sleep(250);
|
||||
Assert.Empty(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
|
||||
|
||||
// Select items
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
// Select simple items
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(1) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Single(s.Driver.FindElements(By.CssSelector("#CartItems tr")));
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(2) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(2, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("3,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select item with inventory - two of it
|
||||
Assert.Equal("5 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(3, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("5,40 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select items with minimum amount
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(4, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("7,20 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select items with adjusted minimum amount
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) input[name='amount']")).Clear();
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) input[name='amount']")).SendKeys("2.3");
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(5) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(5, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("9,50 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select items with custom amount
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).Clear();
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).SendKeys(".2");
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(6, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("9,70 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Select items with another custom amount
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).Clear();
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) input[name='amount']")).SendKeys(".3");
|
||||
s.Driver.FindElement(By.CssSelector(".posItem:nth-child(6) .btn-primary")).Click();
|
||||
Thread.Sleep(250);
|
||||
Assert.Equal(7, s.Driver.FindElements(By.CssSelector("#CartItems tr")).Count);
|
||||
Assert.Equal("10,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Discount: 10%
|
||||
s.Driver.ElementDoesNotExist(By.Id("CartDiscount"));
|
||||
s.Driver.FindElement(By.Id("Discount")).SendKeys("10");
|
||||
Assert.Contains("10% = 0,30 €", s.Driver.FindElement(By.Id("CartDiscount")).Text);
|
||||
Assert.Equal("2,70 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
Assert.Contains("10% = 1,00 €", s.Driver.FindElement(By.Id("CartDiscount")).Text);
|
||||
Assert.Equal("9,00 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Tip: 10%
|
||||
s.Driver.ElementDoesNotExist(By.Id("CartTip"));
|
||||
s.Driver.FindElement(By.Id("Tip-10")).Click();
|
||||
Assert.Contains("10% = 0,27 €", s.Driver.FindElement(By.Id("CartTip")).Text);
|
||||
Assert.Equal("2,97 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
Assert.Contains("10% = 0,90 €", s.Driver.FindElement(By.Id("CartTip")).Text);
|
||||
Assert.Equal("9,90 €", s.Driver.FindElement(By.Id("CartTotal")).Text);
|
||||
|
||||
// Pay
|
||||
// Check values on checkout page
|
||||
s.Driver.FindElement(By.Id("CartSubmit")).Click();
|
||||
s.Driver.WaitUntilAvailable(By.Id("Checkout-v2"));
|
||||
s.Driver.FindElement(By.Id("DetailsToggle")).Click();
|
||||
s.Driver.WaitForElement(By.Id("PaymentDetails-TotalFiat"));
|
||||
Assert.Contains("2,97 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
Assert.Contains("9,90 €", s.Driver.FindElement(By.Id("PaymentDetails-TotalFiat")).Text);
|
||||
|
||||
// Pay
|
||||
s.PayInvoice();
|
||||
|
||||
// Check inventory got updated and is now 3 instead of 5
|
||||
s.Driver.Navigate().GoToUrl(posUrl);
|
||||
Assert.Equal("3 left", s.Driver.FindElement(By.CssSelector(".posItem:nth-child(3) .badge.inventory")).Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -16,12 +16,14 @@ using BTCPayServer.Storage.Models;
|
||||
using BTCPayServer.Storage.Services.Providers.AzureBlobStorage.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using NBitcoin;
|
||||
using NBitpayClient;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using static BTCPayServer.HostedServices.PullPaymentHostedService.PayoutApproval;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
@ -177,7 +179,7 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains(rates, e => e.CurrencyPair == new CurrencyPair("XMR", "BTC") && e.BidAsk.Bid < 1.0m);
|
||||
|
||||
// Check we didn't skip too many exchanges
|
||||
Assert.InRange(skipped, 0, 3);
|
||||
Assert.InRange(skipped, 0, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -289,10 +291,38 @@ retry:
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanGetRateFromRecommendedExchanges()
|
||||
{
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
var b = new StoreBlob();
|
||||
string[] temporarilyBroken = { "UGX" };
|
||||
foreach (var k in StoreBlob.RecommendedExchanges)
|
||||
{
|
||||
b.DefaultCurrency = k.Key;
|
||||
var rules = b.GetDefaultRateRules(provider);
|
||||
var pairs = new[] { CurrencyPair.Parse($"BTC_{k.Key}") }.ToHashSet();
|
||||
var result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
if (temporarilyBroken.Contains(k.Key))
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {key} because it is marked as temporarily broken");
|
||||
continue;
|
||||
}
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanGetRateCryptoCurrenciesByDefault()
|
||||
{
|
||||
string[] brokenShitcoins = { };
|
||||
using var cts = new CancellationTokenSource(60_000);
|
||||
var provider = new BTCPayNetworkProvider(ChainName.Mainnet);
|
||||
var factory = FastTests.CreateBTCPayRateFactory();
|
||||
var fetcher = new RateFetcher(factory);
|
||||
@ -301,37 +331,25 @@ retry:
|
||||
.Select(c => new CurrencyPair(c.CryptoCode, "USD"))
|
||||
.ToHashSet();
|
||||
|
||||
string[] brokenShitcoins = { "BTG", "BTX" };
|
||||
bool IsBrokenShitcoin(CurrencyPair p) => brokenShitcoins.Contains(p.Left) || brokenShitcoins.Contains(p.Right);
|
||||
foreach (var _ in brokenShitcoins)
|
||||
{
|
||||
foreach (var p in pairs.Where(IsBrokenShitcoin).ToArray())
|
||||
{
|
||||
TestLogs.LogInformation($"Skipping {p} because it is marked as broken");
|
||||
pairs.Remove(p);
|
||||
}
|
||||
}
|
||||
|
||||
var rules = new StoreBlob().GetDefaultRateRules(provider);
|
||||
var result = fetcher.FetchRates(pairs, rules, default);
|
||||
var result = fetcher.FetchRates(pairs, rules, cts.Token);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
|
||||
var b = new StoreBlob();
|
||||
foreach (var k in StoreBlob.RecommendedExchanges)
|
||||
{
|
||||
b.DefaultCurrency = k.Key;
|
||||
rules = b.GetDefaultRateRules(provider);
|
||||
pairs =
|
||||
provider.GetAll()
|
||||
.Select(c => new CurrencyPair(c.CryptoCode, k.Key))
|
||||
.ToHashSet();
|
||||
result = fetcher.FetchRates(pairs, rules, default);
|
||||
foreach ((CurrencyPair key, Task<RateResult> value) in result)
|
||||
{
|
||||
var rateResult = await value;
|
||||
TestLogs.LogInformation($"Testing {key} when default currency is {k.Key}");
|
||||
if (brokenShitcoins.Contains(key.ToString()))
|
||||
continue;
|
||||
Assert.True(rateResult.BidAsk != null, $"Impossible to get the rate {rateResult.EvaluatedRule}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -373,7 +391,8 @@ retry:
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "tom-select", "tom-select.complete.min.js").Trim();
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@2.2.2/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
version = Regex.Match(actual, "Tom Select v([0-9]+.[0-9]+.[0-9]+)").Groups[1].Value;
|
||||
expected = (await (await client.GetAsync($"https://cdn.jsdelivr.net/npm/tom-select@{version}/dist/js/tom-select.complete.min.js")).Content.ReadAsStringAsync()).Trim();
|
||||
EqualJsContent(expected, actual);
|
||||
|
||||
actual = GetFileContent("BTCPayServer", "wwwroot", "vendor", "dom-confetti", "dom-confetti.min.js").Trim();
|
||||
|
@ -1700,109 +1700,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanExportInvoicesJson()
|
||||
{
|
||||
decimal GetFieldValue(string input, string fieldName)
|
||||
{
|
||||
var match = Regex.Match(input, $"\"{fieldName}\":([^,]*)");
|
||||
Assert.True(match.Success);
|
||||
return decimal.Parse(match.Groups[1].Value.Trim(), CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
async Task<object[]> GetExport(TestAccount account, string storeId = null)
|
||||
{
|
||||
var content = await account.GetController<UIInvoiceController>(false)
|
||||
.Export("json", storeId);
|
||||
var result = Assert.IsType<ContentResult>(content);
|
||||
Assert.Equal("application/json", result.ContentType);
|
||||
return JsonConvert.DeserializeObject<object[]>(result.Content ?? "[]");
|
||||
}
|
||||
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = await user.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 10,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var networkFee = new FeeRate(invoice.MinerFees["BTC"].SatoshiPerBytes).GetFee(100);
|
||||
var result = await GetExport(user);
|
||||
Assert.Single(result);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - 3 * networkFee;
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
Thread.Sleep(1000); // prevent race conditions, ordering payments
|
||||
// look if you can reduce thread sleep, this was min value for me
|
||||
|
||||
// should reduce invoice due by 0 USD because payment = network fee
|
||||
cashCow.SendToAddress(invoiceAddress, networkFee);
|
||||
Thread.Sleep(1000);
|
||||
|
||||
// pay remaining amount
|
||||
cashCow.SendToAddress(invoiceAddress, 4 * networkFee);
|
||||
Thread.Sleep(1000);
|
||||
|
||||
await TestUtils.EventuallyAsync(async () =>
|
||||
{
|
||||
var parsedJson = await GetExport(user);
|
||||
Assert.Equal(3, parsedJson.Length);
|
||||
|
||||
var invoiceDueAfterFirstPayment = 3 * networkFee.ToDecimal(MoneyUnit.BTC) * invoice.Rate;
|
||||
var pay1str = parsedJson[0].ToString();
|
||||
Assert.Contains("\"InvoiceItemDesc\": \"Some \\\", description\"", pay1str);
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay1str, "InvoiceDue"));
|
||||
Assert.Contains("\"InvoicePrice\": 10.0", pay1str);
|
||||
Assert.Contains("\"ConversionRate\": 5000.0", pay1str);
|
||||
Assert.Contains($"\"InvoiceId\": \"{invoice.Id}\",", pay1str);
|
||||
|
||||
var pay2str = parsedJson[1].ToString();
|
||||
Assert.Equal(invoiceDueAfterFirstPayment, GetFieldValue(pay2str, "InvoiceDue"));
|
||||
|
||||
var pay3str = parsedJson[2].ToString();
|
||||
Assert.Contains("\"InvoiceDue\": 0", pay3str);
|
||||
});
|
||||
|
||||
// create an invoice for a new store and check responses with and without store id
|
||||
var otherUser = tester.NewAccount();
|
||||
await otherUser.GrantAccessAsync();
|
||||
otherUser.RegisterDerivationScheme("BTC");
|
||||
await otherUser.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var newInvoice = await otherUser.BitPay.CreateInvoiceAsync(
|
||||
new Invoice
|
||||
{
|
||||
Price = 21,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
await otherUser.PayInvoice(newInvoice.Id);
|
||||
Assert.Single(await GetExport(otherUser));
|
||||
Assert.Single(await GetExport(otherUser, otherUser.StoreId));
|
||||
Assert.Equal(3, (await GetExport(user, user.StoreId)).Length);
|
||||
Assert.Equal(3, (await GetExport(user)).Length);
|
||||
|
||||
await otherUser.AddOwner(user.UserId);
|
||||
Assert.Equal(4, (await GetExport(user)).Length);
|
||||
Assert.Single(await GetExport(user, otherUser.StoreId));
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanChangeNetworkFeeMode()
|
||||
@ -1892,45 +1789,6 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanExportInvoicesCsv()
|
||||
{
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
await user.GrantAccessAsync();
|
||||
user.RegisterDerivationScheme("BTC");
|
||||
await user.SetNetworkFeeMode(NetworkFeeMode.Always);
|
||||
var invoice = user.BitPay.CreateInvoice(
|
||||
new Invoice
|
||||
{
|
||||
Price = 500,
|
||||
Currency = "USD",
|
||||
PosData = "posData",
|
||||
OrderId = "orderId",
|
||||
ItemDesc = "Some \", description",
|
||||
FullNotifications = true
|
||||
}, Facade.Merchant);
|
||||
|
||||
var cashCow = tester.ExplorerNode;
|
||||
var invoiceAddress = BitcoinAddress.Create(invoice.CryptoInfo[0].Address, cashCow.Network);
|
||||
var firstPayment = invoice.CryptoInfo[0].TotalDue - Money.Coins(0.001m);
|
||||
cashCow.SendToAddress(invoiceAddress, firstPayment);
|
||||
TestUtils.Eventually(() =>
|
||||
{
|
||||
var exportResultPaid =
|
||||
user.GetController<UIInvoiceController>().Export("csv").GetAwaiter().GetResult();
|
||||
var paidresult = Assert.IsType<ContentResult>(exportResultPaid);
|
||||
Assert.Equal("application/csv", paidresult.ContentType);
|
||||
Assert.Contains($",orderId,{invoice.Id},", paidresult.Content);
|
||||
Assert.Contains($",On-Chain,BTC,0.0991,0.0001,5000.0", paidresult.Content);
|
||||
Assert.Contains($",USD,5.00", paidresult.Content); // Seems hacky but some plateform does not render this decimal the same
|
||||
Assert.Contains("0,,\"Some \"\", description\",New (paidPartial),new,paidPartial",
|
||||
paidresult.Content);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanCreateAndDeleteApps()
|
||||
@ -2859,7 +2717,7 @@ namespace BTCPayServer.Tests
|
||||
using var tester = CreateServerTester();
|
||||
await tester.StartAsync();
|
||||
var user = tester.NewAccount();
|
||||
user.GrantAccess();
|
||||
await user.GrantAccessAsync();
|
||||
var controller = tester.PayTester.GetController<UIServerController>(user.UserId, user.StoreId);
|
||||
|
||||
var fileSystemStorageConfiguration = Assert.IsType<FileSystemStorageConfiguration>(Assert
|
||||
@ -2874,7 +2732,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Equal(StorageProvider.FileSystem,
|
||||
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
|
||||
|
||||
|
||||
await CanUploadRemoveFiles(controller);
|
||||
}
|
||||
|
||||
@ -2906,7 +2763,7 @@ namespace BTCPayServer.Tests
|
||||
|
||||
//create a temporary link to file
|
||||
var tmpLinkGenerate = Assert.IsType<RedirectToActionResult>(await controller.CreateTemporaryFileUrl(fileId,
|
||||
new UIServerController.CreateTemporaryFileUrlViewModel()
|
||||
new UIServerController.CreateTemporaryFileUrlViewModel
|
||||
{
|
||||
IsDownload = true,
|
||||
TimeAmount = 1,
|
||||
|
@ -163,7 +163,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.05-dev
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -190,7 +190,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.05-dev
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -224,7 +224,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -259,7 +259,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -149,7 +149,7 @@ services:
|
||||
- "bitcoin_datadir:/data"
|
||||
|
||||
customer_lightningd:
|
||||
image: btcpayserver/lightning:v23.05-dev
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
stop_signal: SIGKILL
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@ -176,7 +176,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
merchant_lightningd:
|
||||
image: btcpayserver/lightning:v23.05-dev
|
||||
image: btcpayserver/lightning:v23.08-dev
|
||||
stop_signal: SIGKILL
|
||||
environment:
|
||||
EXPOSE_TCP: "true"
|
||||
@ -211,7 +211,7 @@ services:
|
||||
- "5432"
|
||||
|
||||
merchant_lnd:
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
@ -248,7 +248,7 @@ services:
|
||||
- bitcoind
|
||||
|
||||
customer_lnd:
|
||||
image: btcpayserver/lnd:v0.16.4-beta
|
||||
image: btcpayserver/lnd:v0.16.4-beta-1
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LND_CHAIN: "btc"
|
||||
|
@ -48,19 +48,19 @@
|
||||
<PackageReference Include="YamlDotNet" Version="8.0.0" />
|
||||
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
|
||||
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.29" />
|
||||
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.4.31" />
|
||||
<PackageReference Include="CsvHelper" Version="15.0.5" />
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
<PackageReference Include="Fido2" Version="2.0.2" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="2.0.2" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="5.0.372" />
|
||||
<PackageReference Include="LNURL" Version="0.0.29" />
|
||||
<PackageReference Include="LNURL" Version="0.0.33" />
|
||||
<PackageReference Include="MailKit" Version="3.3.0" />
|
||||
<PackageReference Include="BTCPayServer.NETCore.Plugins.Mvc" Version="1.4.4" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
<PackageReference Include="System.IO.Pipelines" Version="6.0.3" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.39" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="2.0.0" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="2.0.0" />
|
||||
<PackageReference Include="NicolasDorier.RateLimits" Version="1.2.3" />
|
||||
@ -137,7 +137,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Watch Include="Views\**\*.*"></Watch>
|
||||
<Content Remove="Views\UIReports\StoreReports.cshtml" />
|
||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
|
||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||
|
14
BTCPayServer/Blazor/BlazorExtensions.cs
Normal file
14
BTCPayServer/Blazor/BlazorExtensions.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace BTCPayServer.Blazor
|
||||
{
|
||||
public static class BlazorExtensions
|
||||
{
|
||||
public static bool IsPreRendering(this IJSRuntime runtime)
|
||||
{
|
||||
// The peculiar thing in prerender is that Blazor circuit isn't yet created, so we can't use JSInterop
|
||||
return !(bool)runtime.GetType().GetProperty("IsInitialized").GetValue(runtime);
|
||||
}
|
||||
}
|
||||
}
|
22
BTCPayServer/Blazor/Icon.razor
Normal file
22
BTCPayServer/Blazor/Icon.razor
Normal file
@ -0,0 +1,22 @@
|
||||
@using BTCPayServer.Abstractions.Extensions;
|
||||
@using BTCPayServer.Configuration;
|
||||
@using Microsoft.AspNetCore.Hosting;
|
||||
@using Microsoft.AspNetCore.Mvc.Routing;
|
||||
@using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||
@using Microsoft.AspNetCore.Mvc;
|
||||
@inject IFileVersionProvider FileVersionProvider
|
||||
@inject BTCPayServerOptions BTCPayServerOptions
|
||||
|
||||
<svg role="img" class="icon icon-@Symbol">
|
||||
<use href="@GetPathTo(Symbol)"></use>
|
||||
</svg>
|
||||
@code {
|
||||
public string GetPathTo(string symbol)
|
||||
{
|
||||
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");
|
||||
var rootPath = (BTCPayServerOptions.RootPath ?? "/").WithTrailingSlash();
|
||||
return $"{rootPath}{versioned}#{Symbol}";
|
||||
}
|
||||
[Parameter]
|
||||
public string Symbol { get; set; }
|
||||
}
|
152
BTCPayServer/Blazor/NotificationsDropDown.razor
Normal file
152
BTCPayServer/Blazor/NotificationsDropDown.razor
Normal file
@ -0,0 +1,152 @@
|
||||
@using System.Security.Claims
|
||||
@using BTCPayServer.Abstractions.Contracts;
|
||||
@using BTCPayServer.Configuration;
|
||||
@using BTCPayServer.Data;
|
||||
@using BTCPayServer.Services.Notifications;
|
||||
@using Microsoft.AspNetCore.Identity;
|
||||
@using Microsoft.AspNetCore.Routing;
|
||||
@implements IDisposable
|
||||
@inject AuthenticationStateProvider _AuthenticationStateProvider
|
||||
@inject NotificationManager _NotificationManager
|
||||
@inject UserManager<ApplicationUser> _UserManager
|
||||
@inject IJSRuntime _JSRuntime
|
||||
@inject LinkGenerator _LinkGenerator
|
||||
@inject BTCPayServerOptions _BTCPayServerOptions
|
||||
@inject EventAggregator _EventAggregator
|
||||
|
||||
<div id="Notifications">
|
||||
@if (UnseenCount == "0")
|
||||
{
|
||||
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
|
||||
<Icon Symbol="notifications" />
|
||||
</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
|
||||
<Icon Symbol="notifications" />
|
||||
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
|
||||
</button>
|
||||
}
|
||||
@if (UnseenCount != "0" && Last5 is not null)
|
||||
{
|
||||
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
|
||||
<h5 class="m-0">Notifications</h5>
|
||||
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen">Mark all as seen</a>
|
||||
</div>
|
||||
<div id="NotificationsList" v-pre>
|
||||
@foreach (var n in Last5)
|
||||
{
|
||||
<a href="@NotificationUrl(n.Id)" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
|
||||
<div class="me-3">
|
||||
<Icon Symbol="@NotificationIcon(n.Identifier)" />
|
||||
</div>
|
||||
<div class="notification-item__content">
|
||||
<div class="text-start text-wrap">
|
||||
@n.Body
|
||||
</div>
|
||||
<div class="text-start d-flex">
|
||||
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="p-3">
|
||||
<a href="@NotificationsUrl">View all</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
string NotificationsUrl => _LinkGenerator.GetPathByAction("Index", "UINotifications", pathBase: _BTCPayServerOptions.RootPath);
|
||||
string NotificationUrl(string notificationId) => _LinkGenerator.GetPathByAction("NotificationPassThrough", "UINotifications", values: new { id = notificationId }, pathBase: _BTCPayServerOptions.RootPath);
|
||||
string UnseenCount;
|
||||
List<NotificationViewModel> Last5;
|
||||
IDisposable _EventAggregatorListener;
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (_JSRuntime.IsPreRendering())
|
||||
return;
|
||||
_EventAggregatorListener = _EventAggregator.Subscribe<UserNotificationsUpdatedEvent>((s, evt) =>
|
||||
{
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
|
||||
UpdateState(res);
|
||||
StateHasChanged();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public void Dispose() => _EventAggregatorListener?.Dispose();
|
||||
string SeenCount(int? count)
|
||||
{
|
||||
if (count is not int c)
|
||||
return "0";
|
||||
if (c >= NotificationManager.MaxUnseen)
|
||||
return $"{NotificationManager.MaxUnseen - 1}+";
|
||||
return c.ToString();
|
||||
}
|
||||
void UpdateState((List<NotificationViewModel> Items, int? Count) res)
|
||||
{
|
||||
UnseenCount = SeenCount(res.Count);
|
||||
Last5 = res.Items;
|
||||
}
|
||||
protected async override Task OnParametersSetAsync()
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
// For prerendering and first rendering, always use the cached value
|
||||
var res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: true);
|
||||
// If we forget to update the state here, the UI will flicker.
|
||||
// Because the first rendering will think there is 0 events, until the DB call ends and the second rendering happens.
|
||||
// By updating the state here, the first rendering will show the cached value until the second rendering happens
|
||||
UpdateState(res);
|
||||
// We don't want to block the pre-rendering, so we will render again when the costly request is over
|
||||
if (!_JSRuntime.IsPreRendering())
|
||||
{
|
||||
res = await _NotificationManager.GetSummaryNotifications(userId, cachedOnly: false);
|
||||
UpdateState(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
async Task<string>
|
||||
GetUserId()
|
||||
{
|
||||
var state = await _AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
if (!state.User.Identity.IsAuthenticated)
|
||||
return null;
|
||||
return _UserManager.GetUserId(state.User);
|
||||
}
|
||||
public async Task MarkAllAsSeen()
|
||||
{
|
||||
if (await GetUserId() is string userId)
|
||||
{
|
||||
await _NotificationManager.ToggleSeen(new NotificationsQuery() { Seen = false, UserId = userId }, true);
|
||||
UnseenCount = "0";
|
||||
}
|
||||
}
|
||||
private static string NotificationIcon(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"invoice_expired" => "notifications-invoice-failure",
|
||||
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
|
||||
"invoice_failedtoconfirm" => "notifications-invoice-failure",
|
||||
"invoice_confirmed" => "notifications-invoice-settled",
|
||||
"invoice_paidafterexpiration" => "notifications-invoice-settled",
|
||||
"external-payout-transaction" => "notifications-payout",
|
||||
"payout_awaitingapproval" => "notifications-payout",
|
||||
"payout_awaitingpayment" => "notifications-payout-approved",
|
||||
"newversion" => "notifications-new-version",
|
||||
_ => "note"
|
||||
};
|
||||
}
|
||||
}
|
9
BTCPayServer/Blazor/_Imports.razor
Normal file
9
BTCPayServer/Blazor/_Imports.razor
Normal file
@ -0,0 +1,9 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.JSInterop
|
||||
@using BTCPayServer.Blazor
|
||||
@using BTCPayServer.Abstractions.Extensions
|
@ -1,27 +1,24 @@
|
||||
if (!window.appSales) {
|
||||
window.appSales =
|
||||
{
|
||||
dataLoaded: function (model) {
|
||||
const id = "AppSales-" + model.id;
|
||||
window.appSales = {
|
||||
dataLoaded (model) {
|
||||
const id = `AppSales-${model.id}`;
|
||||
const appId = model.id;
|
||||
const period = model.period;
|
||||
const baseUrl = model.url;
|
||||
const baseUrl = model.dataUrl;
|
||||
const data = model;
|
||||
|
||||
const render = (data, period) => {
|
||||
const series = data.series.map(s => s.salesCount);
|
||||
const labels = data.series.map((s, i) => period === model.period ? s.label : (i % 5 === 0 ? s.label : ''));
|
||||
const labels = data.series.map((s, i) => period === 'Month' ? (i % 5 === 0 ? s.label : '') : s.label);
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const low = min === max ? 0 : Math.max(min - ((max - min) / 5), 0);
|
||||
|
||||
document.querySelectorAll(`#${id} .sales-count`).innerText = data.salesCount;
|
||||
|
||||
new Chartist.Bar(`#${id} .ct-chart`, {
|
||||
labels,
|
||||
series: [series]
|
||||
}, {
|
||||
low,
|
||||
low
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -27,8 +27,8 @@
|
||||
const response = await fetch(url);
|
||||
if (response.ok) {
|
||||
document.getElementById(`AppTopItems-${appId}`).outerHTML = await response.text();
|
||||
const data = document.querySelector(`#AppSales-${appId} template`);
|
||||
if (data) window.appSales.dataLoaded(JSON.parse(data.innerHTML));
|
||||
const data = document.querySelector(`#AppTopItems-${appId} template`);
|
||||
if (data) window.appTopItems.dataLoaded(JSON.parse(data.innerHTML));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
@ -1,8 +1,7 @@
|
||||
if (!window.appTopItems) {
|
||||
window.appTopItems =
|
||||
{
|
||||
dataLoaded: function (model) {
|
||||
const id = "AppTopItems-" + model.id;
|
||||
window.appTopItems = {
|
||||
dataLoaded (model) {
|
||||
const id = `AppTopItems-${model.id}`;
|
||||
const series = model.salesCount;
|
||||
new Chartist.Bar(`#${id} .ct-chart`, { series }, {
|
||||
distributeSeries: true,
|
||||
|
@ -3,7 +3,7 @@
|
||||
@model BTCPayServer.Components.LabelManager.LabelViewModel
|
||||
@{
|
||||
var elementId = "a" + Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
var fetchUrl = Url.Action("GetLabels", "UIWallets", new {
|
||||
var fetchUrl = Url.Action("LabelsJson", "UIWallets", new {
|
||||
walletId = Model.WalletObjectId.WalletId,
|
||||
excludeTypes = Safe.Json(Model.ExcludeTypes)
|
||||
});
|
||||
|
@ -6,7 +6,10 @@
|
||||
@using BTCPayServer.Views.Wallets
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Components.ThemeSwitch
|
||||
@using BTCPayServer.Components.UIExtensionPoint
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Views.CustodianAccounts
|
||||
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@ -90,7 +93,7 @@
|
||||
|
||||
</li>
|
||||
}
|
||||
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
|
||||
<vc:ui-extension-point location="store-wallets-nav" model="@Model"/>
|
||||
@if (PoliciesSettings.Experimental)
|
||||
{
|
||||
@foreach (var custodianAccount in Model.CustodianAccounts)
|
||||
@ -187,6 +190,14 @@
|
||||
<span>Manage Plugins</span>
|
||||
</a>
|
||||
</li>
|
||||
@if (Model.Store != null && Model.ArchivedAppsCount > 0)
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyStoreSettings">
|
||||
<a asp-area="" asp-controller="UIApps" asp-action="ListApps" asp-route-storeId="@Model.Store.Id" asp-route-archived="true" class="nav-link @ViewData.IsActivePage(AppsNavPages.Index)" id="Nav-ArchivedApps">
|
||||
@Model.ArchivedAppsCount Archived App@(Model.ArchivedAppsCount == 1 ? "" : "s")
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -68,13 +68,17 @@ namespace BTCPayServer.Components.MainNav
|
||||
vm.LightningNodes = lightningNodes;
|
||||
|
||||
// Apps
|
||||
var apps = await _appService.GetAllApps(UserId, false, store.Id);
|
||||
vm.Apps = apps.Select(a => new StoreApp
|
||||
{
|
||||
Id = a.Id,
|
||||
AppName = a.AppName,
|
||||
AppType = a.AppType
|
||||
}).ToList();
|
||||
var apps = await _appService.GetAllApps(UserId, false, store.Id, true);
|
||||
vm.Apps = apps
|
||||
.Where(a => !a.Archived)
|
||||
.Select(a => new StoreApp
|
||||
{
|
||||
Id = a.Id,
|
||||
AppName = a.AppName,
|
||||
AppType = a.AppType
|
||||
}).ToList();
|
||||
|
||||
vm.ArchivedAppsCount = apps.Count(a => a.Archived);
|
||||
|
||||
if (PoliciesSettings.Experimental)
|
||||
{
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Services.Apps;
|
||||
|
||||
namespace BTCPayServer.Components.MainNav
|
||||
{
|
||||
@ -13,6 +12,7 @@ namespace BTCPayServer.Components.MainNav
|
||||
public List<StoreApp> Apps { get; set; }
|
||||
public CustodianAccountData[] CustodianAccounts { get; set; }
|
||||
public bool AltcoinsBuild { get; set; }
|
||||
public int ArchivedAppsCount { get; set; }
|
||||
}
|
||||
|
||||
public class StoreApp
|
||||
|
@ -1,31 +0,0 @@
|
||||
@using BTCPayServer.Views.Notifications
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model BTCPayServer.Components.Notifications.NotificationsViewModel
|
||||
|
||||
<div id="Notifications">
|
||||
@if (Model.UnseenCount > 0)
|
||||
{
|
||||
<button id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications" type="button" data-bs-toggle="dropdown">
|
||||
<vc:icon symbol="notifications" />
|
||||
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@Model.UnseenCount</span>
|
||||
</button>
|
||||
<div class="dropdown-menu text-center" id="NotificationsDropdown" aria-labelledby="NotificationsHandle">
|
||||
<div class="d-flex gap-3 align-items-center justify-content-between py-3 px-4 border-bottom border-light">
|
||||
<h5 class="m-0">Notifications</h5>
|
||||
<form id="notificationsForm" asp-controller="UINotifications" asp-action="MarkAllAsSeen" asp-route-returnUrl="@Model.ReturnUrl" method="post">
|
||||
<button class="btn btn-link p-0" type="submit">Mark all as seen</button>
|
||||
</form>
|
||||
</div>
|
||||
<partial name="Components/Notifications/List" model="Model"/>
|
||||
<div class="p-3">
|
||||
<a asp-controller="UINotifications" asp-action="Index">View all</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-controller="UINotifications" asp-action="Index" id="NotificationsHandle" class="mainMenuButton @ViewData.IsActiveCategory(typeof(NotificationsNavPages))" title="Notifications">
|
||||
<vc:icon symbol="notifications" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
@ -1,38 +0,0 @@
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@model BTCPayServer.Components.Notifications.NotificationsViewModel
|
||||
@functions {
|
||||
private static string NotificationIcon(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"invoice_expired" => "notifications-invoice-failure",
|
||||
"invoice_expiredpaidpartial" => "notifications-invoice-failure",
|
||||
"invoice_failedtoconfirm" => "notifications-invoice-failure",
|
||||
"invoice_confirmed" => "notifications-invoice-settled",
|
||||
"invoice_paidafterexpiration" => "notifications-invoice-settled",
|
||||
"external-payout-transaction" => "notifications-payout",
|
||||
"payout_awaitingapproval" => "notifications-payout",
|
||||
"payout_awaitingpayment" => "notifications-payout-approved",
|
||||
"newversion" => "notifications-new-version",
|
||||
_ => "note"
|
||||
};
|
||||
}
|
||||
}
|
||||
<div id="NotificationsList">
|
||||
@foreach (var n in Model.Last5)
|
||||
{
|
||||
<a asp-action="NotificationPassThrough" asp-controller="UINotifications" asp-route-id="@n.Id" class="notification d-flex align-items-center dropdown-item border-bottom border-light py-3 px-4">
|
||||
<div class="me-3">
|
||||
<vc:icon symbol="@NotificationIcon(n.Identifier)" />
|
||||
</div>
|
||||
<div class="notification-item__content">
|
||||
<div class="text-start text-wrap">
|
||||
@n.Body
|
||||
</div>
|
||||
<div class="text-start d-flex">
|
||||
<small class="text-muted" data-timeago-unixms="@n.Created.ToUnixTimeMilliseconds()">@n.Created.ToTimeAgo()</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
}
|
||||
</div>
|
@ -1,27 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Services.Notifications;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Components.Notifications
|
||||
{
|
||||
public class Notifications : ViewComponent
|
||||
{
|
||||
private readonly NotificationManager _notificationManager;
|
||||
|
||||
private static readonly string[] _views = { "List", "Dropdown", "Recent" };
|
||||
|
||||
public Notifications(NotificationManager notificationManager)
|
||||
{
|
||||
_notificationManager = notificationManager;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(string appearance, string returnUrl)
|
||||
{
|
||||
var vm = await _notificationManager.GetSummaryNotifications(UserClaimsPrincipal);
|
||||
vm.ReturnUrl = returnUrl;
|
||||
var viewName = _views.Contains(appearance) ? appearance : _views[0];
|
||||
return View(viewName, vm);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
|
||||
namespace BTCPayServer.Components.Notifications
|
||||
{
|
||||
public class NotificationsViewModel
|
||||
{
|
||||
public string ReturnUrl { get; set; }
|
||||
public int UnseenCount { get; set; }
|
||||
public List<NotificationViewModel> Last5 { get; set; }
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
@model BTCPayServer.Components.Notifications.NotificationsViewModel
|
||||
|
||||
<div id="NotificationsRecent">
|
||||
@if (Model.Last5.Any())
|
||||
{
|
||||
<div class="d-flex align-items-center justify-content-between mb-3">
|
||||
<h4 class="mb-0">Recent Notifications</h4>
|
||||
<a asp-controller="UINotifications" asp-action="Index">View all</a>
|
||||
</div>
|
||||
<partial name="Components/Notifications/List" model="Model"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h4 class="mb-3">Notifications</h4>
|
||||
<p class="text-secondary mt-3">
|
||||
There are no recent unseen notifications.
|
||||
</p>
|
||||
}
|
||||
</div>
|
@ -1,3 +1,4 @@
|
||||
@using BTCPayServer.Client
|
||||
@model BTCPayServer.Components.StoreNumbers.StoreNumbersViewModel
|
||||
|
||||
<div class="widget store-numbers" id="StoreNumbers-@Model.Store.Id">
|
||||
@ -21,26 +22,23 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Paid invoices in the last @Model.TimeframeDays days</h6>
|
||||
@if (Model.PaidInvoices > 0)
|
||||
{
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanViewInvoices">View All</a>
|
||||
}
|
||||
</header>
|
||||
<div class="h3">@Model.PaidInvoices</div>
|
||||
</div>
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Payouts Pending</h6>
|
||||
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id">Manage</a>
|
||||
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanManagePullPayments">Manage</a>
|
||||
</header>
|
||||
<div class="h3">@Model.PayoutsPending</div>
|
||||
</div>
|
||||
@if (Model.Transactions is not null)
|
||||
{
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>TXs in the last @Model.TransactionDays days</h6>
|
||||
@if (Model.Transactions.Value > 0)
|
||||
{
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
|
||||
}
|
||||
</header>
|
||||
<div class="h3">@Model.Transactions.Value</div>
|
||||
</div>
|
||||
}
|
||||
<div class="store-number">
|
||||
<header>
|
||||
<h6>Refunds Issued</h6>
|
||||
|
@ -6,6 +6,7 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Components.StoreRecentTransactions;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Dapper;
|
||||
@ -21,22 +22,16 @@ public class StoreNumbers : ViewComponent
|
||||
{
|
||||
private readonly StoreRepository _storeRepo;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly NBXplorerConnectionFactory _nbxConnectionFactory;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
|
||||
public StoreNumbers(
|
||||
StoreRepository storeRepo,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
BTCPayWalletProvider walletProvider,
|
||||
NBXplorerConnectionFactory nbxConnectionFactory)
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_storeRepo = storeRepo;
|
||||
_walletProvider = walletProvider;
|
||||
_nbxConnectionFactory = nbxConnectionFactory;
|
||||
_networkProvider = networkProvider;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(StoreNumbersViewModel vm)
|
||||
@ -52,28 +47,17 @@ public class StoreNumbers : ViewComponent
|
||||
return View(vm);
|
||||
|
||||
await using var ctx = _dbContextFactory.CreateContext();
|
||||
var payoutsCount = await ctx.Payouts
|
||||
var offset = DateTimeOffset.Now.AddDays(-vm.TimeframeDays).ToUniversalTime();
|
||||
|
||||
vm.PaidInvoices = await _invoiceRepository.GetInvoiceCount(
|
||||
new InvoiceQuery { StoreId = new [] { vm.Store.Id }, StartDate = offset, Status = new [] { "paid", "confirmed" } });
|
||||
vm.PayoutsPending = await ctx.Payouts
|
||||
.Where(p => p.PullPaymentData.StoreId == vm.Store.Id && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingApproval)
|
||||
.CountAsync();
|
||||
var refundsCount = await ctx.Invoices
|
||||
.Where(i => i.StoreData.Id == vm.Store.Id && !i.Archived && i.CurrentRefundId != null)
|
||||
vm.RefundsIssued = await ctx.Invoices
|
||||
.Where(i => i.StoreData.Id == vm.Store.Id && !i.Archived && i.CurrentRefundId != null && i.Created >= offset)
|
||||
.CountAsync();
|
||||
|
||||
var derivation = vm.Store.GetDerivationSchemeSettings(_networkProvider, vm.CryptoCode);
|
||||
int? transactionsCount = null;
|
||||
if (derivation != null && _nbxConnectionFactory.Available)
|
||||
{
|
||||
await using var conn = await _nbxConnectionFactory.OpenConnection();
|
||||
var wid = NBXplorer.Client.DBUtils.nbxv1_get_wallet_id(derivation.Network.CryptoCode, derivation.AccountDerivation.ToString());
|
||||
var afterDate = DateTimeOffset.UtcNow - TimeSpan.FromDays(vm.TransactionDays);
|
||||
var count = await conn.ExecuteScalarAsync<long>("SELECT COUNT(*) FROM wallets_history WHERE code=@code AND wallet_id=@wid AND seen_at > @afterDate", new { code = derivation.Network.CryptoCode, wid, afterDate });
|
||||
transactionsCount = (int)count;
|
||||
}
|
||||
|
||||
vm.PayoutsPending = payoutsCount;
|
||||
vm.Transactions = transactionsCount;
|
||||
vm.RefundsIssued = refundsCount;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ public class StoreNumbersViewModel
|
||||
public StoreData Store { get; set; }
|
||||
public WalletId WalletId { get; set; }
|
||||
public int PayoutsPending { get; set; }
|
||||
public int? Transactions { get; set; }
|
||||
public int TimeframeDays { get; set; } = 7;
|
||||
public int? PaidInvoices { get; set; }
|
||||
public int RefundsIssued { get; set; }
|
||||
public int TransactionDays { get; set; } = 7;
|
||||
public bool InitialRendering { get; set; }
|
||||
public string CryptoCode { get; set; }
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Components.MainLogo
|
||||
@using BTCPayServer.Services
|
||||
@using BTCPayServer.Views.Stores
|
||||
@inject BTCPayServerEnvironment Env
|
||||
@inject IFileService FileService
|
||||
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
|
||||
@ -34,7 +35,7 @@ else
|
||||
<a asp-controller="UIStores" asp-action="Dashboard" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
|
||||
}
|
||||
@if (Model.Options.Any())
|
||||
@if (Model.Options.Any() || Model.ArchivedCount > 0)
|
||||
{
|
||||
<div id="StoreSelector">
|
||||
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
|
||||
@ -64,8 +65,16 @@ else
|
||||
}
|
||||
</li>
|
||||
}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item" id="StoreSelectorCreate">Create Store</a></li>
|
||||
@if (Model.Options.Any())
|
||||
{
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
}
|
||||
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
|
||||
@if (Model.ArchivedCount > 0)
|
||||
{
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.IsActivePage(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,7 +30,9 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
var userId = _userManager.GetUserId(UserClaimsPrincipal);
|
||||
var stores = await _storeRepo.GetStoresByUserId(userId);
|
||||
var currentStore = ViewContext.HttpContext.GetStoreData();
|
||||
var archivedCount = stores.Count(s => s.Archived);
|
||||
var options = stores
|
||||
.Where(store => !store.Archived)
|
||||
.Select(store =>
|
||||
{
|
||||
var cryptoCode = store
|
||||
@ -59,7 +61,8 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
Options = options,
|
||||
CurrentStoreId = currentStore?.Id,
|
||||
CurrentDisplayName = currentStore?.StoreName,
|
||||
CurrentStoreLogoFileId = blob?.LogoFileId
|
||||
CurrentStoreLogoFileId = blob?.LogoFileId,
|
||||
ArchivedCount = archivedCount
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
|
@ -8,6 +8,7 @@ namespace BTCPayServer.Components.StoreSelector
|
||||
public string CurrentStoreId { get; set; }
|
||||
public string CurrentStoreLogoFileId { get; set; }
|
||||
public string CurrentDisplayName { get; set; }
|
||||
public int ArchivedCount { get; set; }
|
||||
}
|
||||
|
||||
public class StoreSelectorOption
|
||||
|
@ -5,7 +5,7 @@
|
||||
@if (Model.Copy) classes += " truncate-center--copy";
|
||||
@if (Model.Elastic) classes += " truncate-center--elastic";
|
||||
}
|
||||
<span class="truncate-center @classes">
|
||||
<span class="truncate-center @classes"@(!string.IsNullOrEmpty(Model.Id) ? $"id={Model.Id}" : null) data-text=@Safe.Json(Model.Text)>
|
||||
@if (Model.IsVue)
|
||||
{
|
||||
<span class="truncate-center-truncated" data-bs-toggle="tooltip" :title=@Safe.Json(Model.Text)>
|
||||
|
@ -15,7 +15,7 @@ namespace BTCPayServer.Components.TruncateCenter;
|
||||
/// <returns>HTML with truncated string</returns>
|
||||
public class TruncateCenter : ViewComponent
|
||||
{
|
||||
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true, bool elastic = false, bool isVue = false)
|
||||
public IViewComponentResult Invoke(string text, string link = null, string classes = null, int padding = 7, bool copy = true, bool elastic = false, bool isVue = false, string id = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return new HtmlContentViewComponentResult(new StringHtmlContent(string.Empty));
|
||||
@ -27,7 +27,8 @@ public class TruncateCenter : ViewComponent
|
||||
IsVue = isVue,
|
||||
Copy = copy,
|
||||
Text = text,
|
||||
Link = link
|
||||
Link = link,
|
||||
Id = id
|
||||
};
|
||||
if (!isVue && text.Length > 2 * padding)
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ namespace BTCPayServer.Components.TruncateCenter
|
||||
public string Text { get; set; }
|
||||
public string Start { get; set; }
|
||||
public string End { get; set; }
|
||||
public string Id { get; set; }
|
||||
public string Classes { get; set; }
|
||||
public string Link { get; set; }
|
||||
public int Padding { get; set; }
|
||||
|
@ -1,15 +1,20 @@
|
||||
@using BTCPayServer.Services;
|
||||
@using BTCPayServer.Views.Stores
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Views.Wallets
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
|
||||
@inject DisplayFormatter DisplayFormatter
|
||||
@model BTCPayServer.Components.WalletNav.WalletNavViewModel
|
||||
|
||||
<div class="d-sm-flex align-items-center justify-content-between">
|
||||
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId" class="unobtrusive-link">
|
||||
<h2 class="mb-1">@Model.Label</h2>
|
||||
<div class="text-muted fw-semibold" data-sensitive>
|
||||
@Model.Balance @Model.Network.CryptoCode
|
||||
@DisplayFormatter.Currency(Model.Balance, Model.Network.CryptoCode)
|
||||
@if (!string.IsNullOrEmpty(Model.BalanceDefaultCurrency))
|
||||
{
|
||||
<span>(@DisplayFormatter.Currency(Model.BalanceDefaultCurrency, Model.DefaultCurrency))</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
<div class="d-flex gap-3 mt-3 mt-sm-0" permission="@Policies.CanModifyStoreSettings">
|
||||
|
@ -1,14 +1,18 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
@ -23,16 +27,22 @@ namespace BTCPayServer.Components.WalletNav
|
||||
{
|
||||
private readonly BTCPayWalletProvider _walletProvider;
|
||||
private readonly UIWalletsController _walletsController;
|
||||
private readonly CurrencyNameTable _currencies;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly RateFetcher _rateFetcher;
|
||||
|
||||
public WalletNav(
|
||||
BTCPayWalletProvider walletProvider,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
UIWalletsController walletsController)
|
||||
UIWalletsController walletsController,
|
||||
CurrencyNameTable currencies,
|
||||
RateFetcher rateFetcher)
|
||||
{
|
||||
_walletProvider = walletProvider;
|
||||
_networkProvider = networkProvider;
|
||||
_walletsController = walletsController;
|
||||
_currencies = currencies;
|
||||
_rateFetcher = rateFetcher;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(WalletId walletId)
|
||||
@ -40,17 +50,34 @@ namespace BTCPayServer.Components.WalletNav
|
||||
var store = ViewContext.HttpContext.GetStoreData();
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>(walletId.CryptoCode);
|
||||
var wallet = _walletProvider.GetWallet(network);
|
||||
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
|
||||
var derivation = store.GetDerivationSchemeSettings(_networkProvider, walletId.CryptoCode);
|
||||
var balance = await _walletsController.GetBalanceString(wallet, derivation?.AccountDerivation);
|
||||
var balance = await wallet.GetBalance(derivation?.AccountDerivation) switch
|
||||
{
|
||||
{ Available: null, Total: var total } => total,
|
||||
{ Available: var available } => available
|
||||
};
|
||||
|
||||
var vm = new WalletNavViewModel
|
||||
{
|
||||
WalletId = walletId,
|
||||
Network = network,
|
||||
Balance = balance,
|
||||
Balance = balance.ShowMoney(network),
|
||||
DefaultCurrency = defaultCurrency,
|
||||
Label = derivation?.Label ?? $"{store.StoreName} {walletId.CryptoCode} Wallet"
|
||||
};
|
||||
|
||||
if (defaultCurrency != network.CryptoCode)
|
||||
{
|
||||
var rule = store.GetStoreBlob().GetRateRules(_networkProvider)?.GetRuleFor(new Rating.CurrencyPair(network.CryptoCode, defaultCurrency));
|
||||
var bid = rule is null ? null : (await _rateFetcher.FetchRate(rule, HttpContext.RequestAborted)).BidAsk?.Bid;
|
||||
if (bid is decimal b)
|
||||
{
|
||||
var currencyData = _currencies.GetCurrencyData(defaultCurrency, true);
|
||||
vm.BalanceDefaultCurrency = (balance.GetValue() * b).ShowMoney(currencyData.Divisibility);
|
||||
}
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,7 @@ namespace BTCPayServer.Components.WalletNav
|
||||
public BTCPayNetwork Network { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Balance { get; set; }
|
||||
public string BalanceDefaultCurrency { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Name = request.AppName,
|
||||
AppType = CrowdfundAppType.AppType
|
||||
AppType = CrowdfundAppType.AppType,
|
||||
Archived = request.Archived ?? false
|
||||
};
|
||||
|
||||
appData.SetSettings(ToCrowdfundSettings(request));
|
||||
@ -97,7 +98,8 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
StoreDataId = storeId,
|
||||
Name = request.AppName,
|
||||
AppType = PointOfSaleAppType.AppType
|
||||
AppType = PointOfSaleAppType.AppType,
|
||||
Archived = request.Archived ?? false
|
||||
};
|
||||
|
||||
appData.SetSettings(ToPointOfSaleSettings(request));
|
||||
@ -111,7 +113,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> UpdatePointOfSaleApp(string appId, CreatePointOfSaleAppRequest request)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType, includeArchived: true);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -129,6 +131,10 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
app.Name = request.AppName;
|
||||
if (request.Archived != null)
|
||||
{
|
||||
app.Archived = request.Archived.Value;
|
||||
}
|
||||
app.SetSettings(ToPointOfSaleSettings(request));
|
||||
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
@ -153,7 +159,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetAllApps()
|
||||
{
|
||||
var apps = await _appService.GetAllApps(_userManager.GetUserId(User));
|
||||
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), includeArchived: true);
|
||||
|
||||
return Ok(apps.Select(ToModel).ToArray());
|
||||
}
|
||||
@ -162,7 +168,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetAllApps(string storeId)
|
||||
{
|
||||
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), allowNoUser: false, storeId);
|
||||
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), false, storeId, true);
|
||||
|
||||
return Ok(apps.Select(ToModel).ToArray());
|
||||
}
|
||||
@ -171,7 +177,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null);
|
||||
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -184,7 +190,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetPosApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType, includeArchived: true);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -197,7 +203,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> GetCrowdfundApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType);
|
||||
var app = await _appService.GetApp(appId, CrowdfundAppType.AppType, includeArchived: true);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -209,7 +215,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[HttpDelete("~/api/v1/apps/{appId}")]
|
||||
public async Task<IActionResult> DeleteApp(string appId)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, null);
|
||||
var app = await _appService.GetApp(appId, null, includeArchived: true);
|
||||
if (app == null)
|
||||
{
|
||||
return AppNotFound();
|
||||
@ -293,6 +299,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return new AppDataBase
|
||||
{
|
||||
Id = appData.Id,
|
||||
Archived = appData.Archived,
|
||||
AppType = appData.AppType,
|
||||
Name = appData.Name,
|
||||
StoreId = appData.StoreDataId,
|
||||
@ -305,6 +312,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return new AppDataBase
|
||||
{
|
||||
Id = appData.Id,
|
||||
Archived = appData.Archived,
|
||||
AppType = appData.AppType,
|
||||
Name = appData.AppName,
|
||||
StoreId = appData.StoreId,
|
||||
@ -319,6 +327,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return new PointOfSaleAppData
|
||||
{
|
||||
Id = appData.Id,
|
||||
Archived = appData.Archived,
|
||||
AppType = appData.AppType,
|
||||
Name = appData.Name,
|
||||
StoreId = appData.StoreDataId,
|
||||
@ -387,6 +396,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return new CrowdfundAppData
|
||||
{
|
||||
Id = appData.Id,
|
||||
Archived = appData.Archived,
|
||||
AppType = appData.AppType,
|
||||
Name = appData.Name,
|
||||
StoreId = appData.StoreDataId,
|
||||
|
@ -442,7 +442,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
[HttpDelete("~/api/v1/stores/{storeId}/pull-payments/{pullPaymentId}")]
|
||||
[Authorize(Policy = Policies.CanManagePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[Authorize(Policy = Policies.CanArchivePullPayments, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> ArchivePullPayment(string storeId, string pullPaymentId)
|
||||
{
|
||||
using var ctx = _dbContextFactory.CreateContext();
|
||||
|
@ -53,7 +53,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
|
||||
private static LightningAutomatedPayoutSettings ToModel(PayoutProcessorData data)
|
||||
{
|
||||
var blob = data.HasTypedBlob<LightningAutomatedPayoutBlob>().GetBlob();
|
||||
var blob = data.HasTypedBlob<LightningAutomatedPayoutBlob>().GetBlob() ?? new LightningAutomatedPayoutBlob();
|
||||
return new LightningAutomatedPayoutSettings()
|
||||
{
|
||||
PaymentMethod = data.PaymentMethod,
|
||||
|
@ -33,6 +33,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_storeRepository = storeRepository;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpGet("~/api/v1/stores")]
|
||||
public Task<ActionResult<IEnumerable<Client.Models.StoreData>>> GetStores()
|
||||
@ -112,7 +113,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
return Ok(FromModel(store));
|
||||
}
|
||||
|
||||
internal static Client.Models.StoreData FromModel(Data.StoreData data)
|
||||
internal static Client.Models.StoreData FromModel(StoreData data)
|
||||
{
|
||||
var storeBlob = data.GetStoreBlob();
|
||||
return new Client.Models.StoreData
|
||||
@ -120,6 +121,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
Id = data.Id,
|
||||
Name = data.StoreName,
|
||||
Website = data.StoreWebsite,
|
||||
Archived = data.Archived,
|
||||
SupportUrl = storeBlob.StoreSupportUrl,
|
||||
SpeedPolicy = data.SpeedPolicy,
|
||||
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToStringNormalized(),
|
||||
@ -166,6 +168,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var blob = model.GetStoreBlob();
|
||||
model.StoreName = restModel.Name;
|
||||
model.StoreWebsite = restModel.Website;
|
||||
model.Archived = restModel.Archived;
|
||||
model.SpeedPolicy = restModel.SpeedPolicy;
|
||||
model.SetDefaultPaymentId(defaultPaymentMethod);
|
||||
//we do not include the default payment method in this model and instead opt to set it in the stores/storeid/payment-methods endpoints
|
||||
|
@ -759,7 +759,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
}
|
||||
|
||||
public override async Task<IEnumerable<OnChainWalletTransactionData>> ShowOnChainWalletTransactions(
|
||||
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null,
|
||||
string storeId, string cryptoCode, TransactionStatus[] statusFilter = null, string labelFilter = null, int skip = 0,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
return GetFromActionResult<IEnumerable<OnChainWalletTransactionData>>(
|
||||
|
@ -48,7 +48,7 @@ public class LightningAddressService
|
||||
{
|
||||
return await _memoryCache.GetOrCreateAsync(GetKey(username), async entry =>
|
||||
{
|
||||
var result = await Get(new LightningAddressQuery() { Usernames = new[] { username } });
|
||||
var result = await Get(new LightningAddressQuery { Usernames = new[] { username } });
|
||||
return result.FirstOrDefault();
|
||||
});
|
||||
}
|
||||
@ -62,7 +62,7 @@ public class LightningAddressService
|
||||
{
|
||||
data.Username = NormalizeUsername(data.Username);
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
var result = (await GetCore(context, new LightningAddressQuery() { Usernames = new[] { data.Username } }))
|
||||
var result = (await GetCore(context, new LightningAddressQuery { Usernames = new[] { data.Username } }))
|
||||
.FirstOrDefault();
|
||||
if (result is not null)
|
||||
{
|
||||
@ -84,7 +84,7 @@ public class LightningAddressService
|
||||
{
|
||||
username = NormalizeUsername(username);
|
||||
await using var context = _applicationDbContextFactory.CreateContext();
|
||||
var x = (await GetCore(context, new LightningAddressQuery() { Usernames = new[] { username } })).FirstOrDefault();
|
||||
var x = (await GetCore(context, new LightningAddressQuery { Usernames = new[] { username } })).FirstOrDefault();
|
||||
if (x is null)
|
||||
return true;
|
||||
if (storeId is not null && x.StoreDataId != storeId)
|
||||
@ -100,7 +100,7 @@ public class LightningAddressService
|
||||
|
||||
public async Task Set(LightningAddressData data, ApplicationDbContext context)
|
||||
{
|
||||
var result = (await GetCore(context, new LightningAddressQuery() { Usernames = new[] { data.Username } }))
|
||||
var result = (await GetCore(context, new LightningAddressQuery { Usernames = new[] { data.Username } }))
|
||||
.FirstOrDefault();
|
||||
if (result is not null)
|
||||
{
|
||||
|
@ -76,28 +76,26 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> ListApps(
|
||||
string storeId,
|
||||
string sortOrder = null,
|
||||
string sortOrderColumn = null
|
||||
string sortOrderColumn = null,
|
||||
bool archived = false
|
||||
)
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, store.Id);
|
||||
var apps = (await _appService.GetAllApps(GetUserId(), false, store.Id, archived))
|
||||
.Where(app => app.Archived == archived);
|
||||
|
||||
if (sortOrder != null && sortOrderColumn != null)
|
||||
{
|
||||
apps = apps.OrderByDescending(app =>
|
||||
{
|
||||
switch (sortOrderColumn)
|
||||
return sortOrderColumn switch
|
||||
{
|
||||
case nameof(app.AppName):
|
||||
return app.AppName;
|
||||
case nameof(app.StoreName):
|
||||
return app.StoreName;
|
||||
case nameof(app.AppType):
|
||||
return app.AppType;
|
||||
default:
|
||||
return app.Id;
|
||||
}
|
||||
}).ToArray();
|
||||
nameof(app.AppName) => app.AppName,
|
||||
nameof(app.StoreName) => app.StoreName,
|
||||
nameof(app.AppType) => app.AppType,
|
||||
_ => app.Id
|
||||
};
|
||||
});
|
||||
|
||||
switch (sortOrder)
|
||||
{
|
||||
@ -105,7 +103,7 @@ namespace BTCPayServer.Controllers
|
||||
ViewData[$"{sortOrderColumn}SortOrder"] = "asc";
|
||||
break;
|
||||
case "asc":
|
||||
apps = apps.Reverse().ToArray();
|
||||
apps = apps.Reverse();
|
||||
ViewData[$"{sortOrderColumn}SortOrder"] = "desc";
|
||||
break;
|
||||
}
|
||||
@ -113,7 +111,7 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return View(new ListAppsViewModel
|
||||
{
|
||||
Apps = apps
|
||||
Apps = apps.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
@ -161,7 +159,6 @@ namespace BTCPayServer.Controllers
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App successfully created";
|
||||
CreatedAppId = appData.Id;
|
||||
|
||||
|
||||
var url = await type.ConfigureLink(appData);
|
||||
return Redirect(url);
|
||||
}
|
||||
@ -190,6 +187,36 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId = app.StoreDataId });
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/archive")]
|
||||
public async Task<IActionResult> ToggleArchive(string appId)
|
||||
{
|
||||
var app = GetCurrentApp();
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var type = _appService.GetAppType(app.AppType);
|
||||
if (type is null)
|
||||
{
|
||||
return UnprocessableEntity();
|
||||
}
|
||||
|
||||
var archived = !app.Archived;
|
||||
if (await _appService.SetArchived(app, archived))
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = archived
|
||||
? "The app has been archived and will no longer appear in the apps list by default."
|
||||
: "The app has been unarchived and will appear in the apps list by default again.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = $"Failed to {(archived ? "archive" : "unarchive")} the app.";
|
||||
}
|
||||
|
||||
var url = await type.ConfigureLink(app);
|
||||
return Redirect(url);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[HttpPost("{appId}/upload-file")]
|
||||
|
@ -24,7 +24,6 @@ using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Invoices.Export;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -123,6 +122,7 @@ namespace BTCPayServer.Controllers
|
||||
var additionalData = metaData
|
||||
.Where(dict => !InvoiceAdditionalDataExclude.Contains(dict.Key))
|
||||
.ToDictionary(dict => dict.Key, dict => dict.Value);
|
||||
|
||||
var model = new InvoiceDetailsModel
|
||||
{
|
||||
StoreId = store.Id,
|
||||
@ -149,7 +149,6 @@ namespace BTCPayServer.Controllers
|
||||
StatusException = invoice.ExceptionStatus,
|
||||
Events = invoice.Events,
|
||||
Metadata = metaData,
|
||||
AdditionalData = additionalData,
|
||||
Archived = invoice.Archived,
|
||||
CanRefund = invoiceState.CanRefund(),
|
||||
Refunds = invoice.Refunds,
|
||||
@ -166,6 +165,27 @@ namespace BTCPayServer.Controllers
|
||||
model.CryptoPayments = details.CryptoPayments;
|
||||
model.Payments = details.Payments;
|
||||
model.Overpaid = details.Overpaid;
|
||||
|
||||
if (additionalData.ContainsKey("receiptData"))
|
||||
{
|
||||
model.ReceiptData = (Dictionary<string, object>)additionalData["receiptData"];
|
||||
additionalData.Remove("receiptData");
|
||||
}
|
||||
|
||||
if (additionalData.ContainsKey("posData") && additionalData["posData"] is string posData)
|
||||
{
|
||||
// overwrite with parsed JSON if possible
|
||||
try
|
||||
{
|
||||
additionalData["posData"] = PosDataParser.ParsePosData(JObject.Parse(posData));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
additionalData["posData"] = posData;
|
||||
}
|
||||
}
|
||||
|
||||
model.AdditionalData = additionalData;
|
||||
|
||||
return View(model);
|
||||
}
|
||||
@ -227,13 +247,13 @@ namespace BTCPayServer.Controllers
|
||||
|
||||
string txId = paymentData.GetPaymentId();
|
||||
string? link = GetTransactionLink(paymentMethodId, txId);
|
||||
|
||||
|
||||
return new ViewPaymentRequestViewModel.PaymentRequestInvoicePayment
|
||||
{
|
||||
Amount = paymentEntity.PaidAmount.Gross,
|
||||
Paid = paymentEntity.PaidAmount.Net,
|
||||
Paid = paymentEntity.InvoicePaidAmount.Net,
|
||||
ReceivedDate = paymentEntity.ReceivedTime.DateTime,
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.PaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaidFormatted = _displayFormatter.Currency(paymentEntity.InvoicePaidAmount.Net, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
RateFormatted = _displayFormatter.Currency(paymentEntity.Rate, i.Currency, DisplayFormatter.CurrencyFormat.Symbol),
|
||||
PaymentMethod = paymentMethodId.ToPrettyString(),
|
||||
Link = link,
|
||||
@ -246,7 +266,7 @@ namespace BTCPayServer.Controllers
|
||||
.Where(payment => payment != null)
|
||||
.ToList();
|
||||
|
||||
vm.Amount = payments.Sum(p => p!.Paid);
|
||||
vm.Amount = i.PaidAmount.Net;
|
||||
vm.Payments = receipt.ShowPayments is false ? null : payments;
|
||||
vm.AdditionalData = PosDataParser.ParsePosData(receiptData);
|
||||
|
||||
@ -1161,42 +1181,6 @@ namespace BTCPayServer.Controllers
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanViewInvoices)]
|
||||
[BitpayAPIConstraint(false)]
|
||||
public async Task<IActionResult> Export(string format, string? storeId = null, string? searchTerm = null, int timezoneOffset = 0)
|
||||
{
|
||||
var model = new InvoiceExport(_CurrencyNameTable);
|
||||
var fs = new SearchString(searchTerm);
|
||||
var storeIds = new HashSet<string>();
|
||||
if (storeId is not null)
|
||||
{
|
||||
storeIds.Add(storeId);
|
||||
}
|
||||
if (fs.GetFilterArray("storeid") is { } l)
|
||||
{
|
||||
foreach (var i in l)
|
||||
storeIds.Add(i);
|
||||
}
|
||||
|
||||
var apps = await _appService.GetAllApps(GetUserId(), false, storeId);
|
||||
InvoiceQuery invoiceQuery = GetInvoiceQuery(fs, apps, timezoneOffset);
|
||||
invoiceQuery.StoreId = storeIds.ToArray();
|
||||
invoiceQuery.Skip = 0;
|
||||
invoiceQuery.Take = int.MaxValue;
|
||||
var invoices = await _InvoiceRepository.GetInvoices(invoiceQuery);
|
||||
var res = model.Process(invoices, format);
|
||||
|
||||
var cd = new ContentDisposition
|
||||
{
|
||||
FileName = $"btcpay-export-{DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}.{format}",
|
||||
Inline = true
|
||||
};
|
||||
Response.Headers.Add("Content-Disposition", cd.ToString());
|
||||
Response.Headers.Add("X-Content-Type-Options", "nosniff");
|
||||
return Content(res, "application/" + format);
|
||||
}
|
||||
|
||||
private SelectList GetPaymentMethodsSelectList()
|
||||
{
|
||||
var store = GetCurrentStore();
|
||||
|
@ -3,7 +3,6 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@ -18,9 +17,9 @@ using BTCPayServer.Data.Payouts.LightningLike;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Lightning;
|
||||
using BTCPayServer.Logging;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Payments.Lightning;
|
||||
using BTCPayServer.Plugins;
|
||||
using BTCPayServer.Plugins.Crowdfund;
|
||||
using BTCPayServer.Plugins.PointOfSale;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
@ -32,7 +31,6 @@ using BTCPayServer.Services.Stores;
|
||||
using LNURL;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using NBitcoin;
|
||||
@ -368,41 +366,56 @@ namespace BTCPayServer
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> ResolveLightningAddress(string username)
|
||||
{
|
||||
if (string.IsNullOrEmpty(username))
|
||||
return NotFound("Unknown username");
|
||||
|
||||
LNURLPayRequest lnurlRequest = null;
|
||||
|
||||
// Check core and fall back to lookup Lightning Address via plugins
|
||||
var lightningAddressSettings = await _lightningAddressService.ResolveByAddress(username);
|
||||
if (lightningAddressSettings is null || username is null)
|
||||
return NotFound("Unknown username");
|
||||
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
var cryptoCode = "BTC";
|
||||
if (store is null)
|
||||
return NotFound("Unknown username");
|
||||
if (GetLNUrlPaymentMethodId(cryptoCode, store, out var lnUrlMethod) is null)
|
||||
return NotFound("LNUrl not available for store");
|
||||
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
|
||||
var lnurlRequest = new LNURLPayRequest()
|
||||
if (lightningAddressSettings is null)
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
|
||||
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
|
||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0
|
||||
};
|
||||
var resolver = (LightningAddressResolver)await _pluginHookService.ApplyFilter("resolve-lnurlp-request-for-lightning-address",
|
||||
new LightningAddressResolver(username));
|
||||
|
||||
lnurlRequest = resolver.LNURLPayRequest;
|
||||
if (lnurlRequest is null)
|
||||
return NotFound("Unknown username");
|
||||
}
|
||||
else
|
||||
{
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
if (store is null)
|
||||
return NotFound("Unknown username");
|
||||
|
||||
var cryptoCode = "BTC";
|
||||
if (GetLNUrlPaymentMethodId(cryptoCode, store, out var lnUrlMethod) is null)
|
||||
return NotFound("LNURL not available for store");
|
||||
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
lnurlRequest = new LNURLPayRequest
|
||||
{
|
||||
Tag = "payRequest",
|
||||
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
|
||||
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
|
||||
CommentAllowed = lnUrlMethod.LUD12Enabled ? 2000 : 0
|
||||
};
|
||||
|
||||
var lnUrlMetadata = new Dictionary<string, string>
|
||||
{
|
||||
["text/identifier"] = $"{username}@{Request.Host}"
|
||||
};
|
||||
SetLNUrlDescriptionMetadata(lnUrlMetadata, store, store.GetStoreBlob(), null);
|
||||
lnurlRequest.Metadata =
|
||||
JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||
|
||||
lnurlRequest.Callback = new Uri(_linkGenerator.GetUriByAction(
|
||||
action: nameof(GetLNURLForLightningAddress),
|
||||
controller: "UILNURL",
|
||||
values: new { cryptoCode, username }, Request.Scheme, Request.Host, Request.PathBase));
|
||||
}
|
||||
|
||||
NormalizeSendable(lnurlRequest);
|
||||
|
||||
var lnUrlMetadata = new Dictionary<string, string>()
|
||||
{
|
||||
["text/identifier"] = $"{username}@{Request.Host}"
|
||||
};
|
||||
SetLNUrlDescriptionMetadata(lnUrlMetadata, store, store.GetStoreBlob(), null);
|
||||
lnurlRequest.Metadata =
|
||||
JsonConvert.SerializeObject(lnUrlMetadata.Select(kv => new[] { kv.Key, kv.Value }));
|
||||
|
||||
lnurlRequest.Callback = new Uri(_linkGenerator.GetUriByAction(
|
||||
action: nameof(GetLNURLForLightningAddress),
|
||||
controller: "UILNURL",
|
||||
values: new { cryptoCode, username }, Request.Scheme, Request.Host, Request.PathBase));
|
||||
|
||||
lnurlRequest = await _pluginHookService.ApplyFilter("modify-lnurlp-request", lnurlRequest) as LNURLPayRequest;
|
||||
return Ok(lnurlRequest);
|
||||
}
|
||||
@ -417,21 +430,23 @@ namespace BTCPayServer
|
||||
return NotFound("Unknown username");
|
||||
var blob = lightningAddressSettings.GetBlob();
|
||||
var store = await _storeRepository.FindStore(lightningAddressSettings.StoreDataId);
|
||||
if (store is null)
|
||||
return NotFound("Unknown username");
|
||||
var result = await GetLNURLRequest(
|
||||
cryptoCode,
|
||||
store,
|
||||
store.GetStoreBlob(),
|
||||
new CreateInvoiceRequest()
|
||||
new CreateInvoiceRequest
|
||||
{
|
||||
Currency = blob?.CurrencyCode,
|
||||
Metadata = blob?.InvoiceMetadata
|
||||
},
|
||||
new LNURLPayRequest()
|
||||
new LNURLPayRequest
|
||||
{
|
||||
MinSendable = blob?.Min is decimal min ? new LightMoney(min, LightMoneyUnit.Satoshi) : null,
|
||||
MaxSendable = blob?.Max is decimal max ? new LightMoney(max, LightMoneyUnit.Satoshi) : null,
|
||||
},
|
||||
new Dictionary<string, string>()
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "text/identifier", $"{username}@{Request.Host}" }
|
||||
});
|
||||
@ -714,6 +729,7 @@ namespace BTCPayServer
|
||||
try
|
||||
{
|
||||
var expiry = i.ExpirationTime.ToUniversalTime() - DateTimeOffset.UtcNow;
|
||||
HttpContext.Items.Add(nameof(invoiceId), invoiceId);
|
||||
var description = (await _pluginHookService.ApplyFilter("modify-lnurlp-description", lnurlPayRequest.Metadata)) as string;
|
||||
if (description is null)
|
||||
return NotFound();
|
||||
|
@ -544,6 +544,8 @@ namespace BTCPayServer.Controllers
|
||||
{$"{Policies.CanViewPaymentRequests}:", ("View your payment requests", "Allows viewing the selected stores' payment requests.")},
|
||||
{Policies.CanManagePullPayments, ("Manage your pull payments", "Allows viewing, modifying, deleting and creating pull payments on all your stores.")},
|
||||
{$"{Policies.CanManagePullPayments}:", ("Manage selected stores' pull payments", "Allows viewing, modifying, deleting and creating pull payments on the selected stores.")},
|
||||
{Policies.CanArchivePullPayments, ("Archive your pull payments", "Allows deleting pull payments on all your stores.")},
|
||||
{$"{Policies.CanArchivePullPayments}:", ("Archive selected stores' pull payments", "Allows deleting pull payments on the selected stores.")},
|
||||
{Policies.CanCreatePullPayments, ("Create pull payments", "Allows creating pull payments on all your stores.")},
|
||||
{$"{Policies.CanCreatePullPayments}:", ("Create pull payments in selected stores", "Allows creating pull payments on the selected stores.")},
|
||||
{Policies.CanCreateNonApprovedPullPayments, ("Create non-approved pull payments", "Allows creating pull payments without automatic approval on all your stores.")},
|
||||
|
@ -23,85 +23,17 @@ namespace BTCPayServer.Controllers
|
||||
[Route("notifications/{action:lowercase=Index}")]
|
||||
public class UINotificationsController : Controller
|
||||
{
|
||||
private readonly BTCPayServerEnvironment _env;
|
||||
private readonly NotificationSender _notificationSender;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly NotificationManager _notificationManager;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
|
||||
public UINotificationsController(BTCPayServerEnvironment env,
|
||||
NotificationSender notificationSender,
|
||||
public UINotificationsController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
NotificationManager notificationManager,
|
||||
EventAggregator eventAggregator)
|
||||
NotificationManager notificationManager)
|
||||
{
|
||||
_env = env;
|
||||
_notificationSender = notificationSender;
|
||||
_userManager = userManager;
|
||||
_notificationManager = notificationManager;
|
||||
_eventAggregator = eventAggregator;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult GetNotificationDropdownUI(string returnUrl)
|
||||
{
|
||||
return ViewComponent("Notifications", new { appearance = "Dropdown", returnUrl });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> SubscribeUpdates(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
var websocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var userId = _userManager.GetUserId(User);
|
||||
var websocketHelper = new WebSocketHelper(websocket);
|
||||
IEventAggregatorSubscription subscription = null;
|
||||
try
|
||||
{
|
||||
subscription = _eventAggregator.SubscribeAsync<UserNotificationsUpdatedEvent>(async evt =>
|
||||
{
|
||||
if (evt.UserId == userId)
|
||||
{
|
||||
await websocketHelper.Send("update");
|
||||
}
|
||||
});
|
||||
|
||||
await websocketHelper.NextMessageAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
catch (WebSocketException)
|
||||
{
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
subscription?.Dispose();
|
||||
await websocketHelper.DisposeAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
return new EmptyResult();
|
||||
}
|
||||
#if DEBUG
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GenerateJunk(int x = 100, bool admin = true)
|
||||
{
|
||||
for (int i = 0; i < x; i++)
|
||||
{
|
||||
await _notificationSender.SendNotification(
|
||||
admin ? (NotificationScope)new AdminScope() : new UserScope(_userManager.GetUserId(User)),
|
||||
new JunkNotification());
|
||||
}
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
#endif
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index(int skip = 0, int count = 50, int timezoneOffset = 0)
|
||||
{
|
||||
|
@ -333,7 +333,7 @@ namespace BTCPayServer.Controllers
|
||||
try
|
||||
{
|
||||
var store = await _storeRepository.FindStore(result.StoreId);
|
||||
var prData = await _PaymentRequestRepository.FindPaymentRequest(result.Id, null);
|
||||
var prData = await _PaymentRequestRepository.FindPaymentRequest(result.Id, null, cancellationToken);
|
||||
var newInvoice = await _InvoiceController.CreatePaymentRequestInvoice(prData, amount, result.AmountDue, store, Request, cancellationToken);
|
||||
if (redirectToInvoice)
|
||||
{
|
||||
|
@ -39,6 +39,7 @@ namespace BTCPayServer.Controllers
|
||||
private readonly PullPaymentHostedService _pullPaymentService;
|
||||
private readonly ApplicationDbContextFactory _dbContextFactory;
|
||||
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
|
||||
public StoreData CurrentStore
|
||||
{
|
||||
@ -54,7 +55,8 @@ namespace BTCPayServer.Controllers
|
||||
DisplayFormatter displayFormatter,
|
||||
PullPaymentHostedService pullPaymentHostedService,
|
||||
ApplicationDbContextFactory dbContextFactory,
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings)
|
||||
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings,
|
||||
IAuthorizationService authorizationService)
|
||||
{
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_payoutHandlers = payoutHandlers;
|
||||
@ -63,10 +65,11 @@ namespace BTCPayServer.Controllers
|
||||
_pullPaymentService = pullPaymentHostedService;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
_jsonSerializerSettings = jsonSerializerSettings;
|
||||
_authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
[HttpGet("stores/{storeId}/pull-payments/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> NewPullPayment(string storeId)
|
||||
{
|
||||
if (CurrentStore is null)
|
||||
@ -95,7 +98,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("stores/{storeId}/pull-payments/new")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanCreateNonApprovedPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> NewPullPayment(string storeId, NewPullPaymentModel model)
|
||||
{
|
||||
if (CurrentStore is null)
|
||||
@ -135,6 +138,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(model);
|
||||
if (model.AutoApproveClaims)
|
||||
{
|
||||
model.AutoApproveClaims = (await
|
||||
_authorizationService.AuthorizeAsync(User, storeId, Policies.CanCreatePullPayments)).Succeeded;
|
||||
}
|
||||
await _pullPaymentService.CreatePullPayment(new HostedServices.CreatePullPayment()
|
||||
{
|
||||
Name = model.Name,
|
||||
@ -248,7 +256,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanArchivePullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public IActionResult ArchivePullPayment(string storeId,
|
||||
string pullPaymentId)
|
||||
{
|
||||
@ -257,11 +265,11 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpPost("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
[Authorize(Policy = Policies.CanArchivePullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
|
||||
public async Task<IActionResult> ArchivePullPaymentPost(string storeId,
|
||||
string pullPaymentId)
|
||||
{
|
||||
await _pullPaymentService.Cancel(new HostedServices.PullPaymentHostedService.CancelRequest(pullPaymentId));
|
||||
await _pullPaymentService.Cancel(new PullPaymentHostedService.CancelRequest(pullPaymentId));
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Message = "Pull payment archived",
|
||||
@ -530,7 +538,8 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var ppBlob = item.PullPayment?.GetBlob();
|
||||
var payoutBlob = item.Payout.GetBlob(_jsonSerializerSettings);
|
||||
string payoutSource;
|
||||
item.Payout.PullPaymentData = item.PullPayment;
|
||||
string payoutSource = item.Payout.GetPayoutSource(_jsonSerializerSettings);
|
||||
if (payoutBlob.Metadata?.TryGetValue("source", StringComparison.InvariantCultureIgnoreCase,
|
||||
out var source) is true)
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services.Mails;
|
||||
using BTCPayServer.Validation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MimeKit;
|
||||
|
||||
@ -75,33 +76,22 @@ namespace BTCPayServer.Controllers
|
||||
if (command.StartsWith("test", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
var rule = vm.Rules[index];
|
||||
if (string.IsNullOrEmpty(rule.Subject) || string.IsNullOrEmpty(rule.Body) || string.IsNullOrEmpty(rule.To))
|
||||
try
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Warning,
|
||||
Message = "Please fill all required fields before testing"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
var emailSettings = blob.EmailSettings;
|
||||
using var client = await emailSettings.CreateSmtpClient();
|
||||
var message = emailSettings.CreateMailMessage(MailboxAddress.Parse(rule.To), "(test) " + rule.Subject, rule.Body, true);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Rule email saved and sent to {rule.To}. Please verify you received it.";
|
||||
var emailSettings = blob.EmailSettings;
|
||||
using var client = await emailSettings.CreateSmtpClient();
|
||||
var message = emailSettings.CreateMailMessage(MailboxAddress.Parse(rule.To), "(test) " + rule.Subject, rule.Body, true);
|
||||
await client.SendAsync(message);
|
||||
await client.DisconnectAsync(true);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Rule email saved and sent to {rule.To}. Please verify you received it.";
|
||||
|
||||
blob.EmailRules = vm.Rules;
|
||||
store.SetStoreBlob(blob);
|
||||
await _Repo.UpdateStore(store);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message;
|
||||
}
|
||||
blob.EmailRules = vm.Rules;
|
||||
store.SetStoreBlob(blob);
|
||||
await _Repo.UpdateStore(store);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Error: " + ex.Message;
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -128,10 +118,18 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
[Required]
|
||||
public WebhookEventType Trigger { get; set; }
|
||||
|
||||
public bool CustomerEmail { get; set; }
|
||||
|
||||
[Required]
|
||||
[MailboxAddress]
|
||||
public string To { get; set; }
|
||||
public string Body { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Body { get; set; }
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/email-settings")]
|
||||
|
@ -680,6 +680,7 @@ namespace BTCPayServer.Controllers
|
||||
InvoiceExpiration = (int)storeBlob.InvoiceExpiration.TotalMinutes,
|
||||
DefaultCurrency = storeBlob.DefaultCurrency,
|
||||
BOLT11Expiration = (long)storeBlob.RefundBOLT11Expiration.TotalDays,
|
||||
Archived = store.Archived,
|
||||
CanDelete = _Repo.CanDeleteStores()
|
||||
};
|
||||
|
||||
@ -827,6 +828,23 @@ namespace BTCPayServer.Controllers
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("{storeId}/archive")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettings)]
|
||||
public async Task<IActionResult> ToggleArchive(string storeId)
|
||||
{
|
||||
CurrentStore.Archived = !CurrentStore.Archived;
|
||||
await _Repo.UpdateStore(CurrentStore);
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = CurrentStore.Archived
|
||||
? "The store has been archived and will no longer appear in the stores list by default."
|
||||
: "The store has been unarchived and will appear in the stores list by default again.";
|
||||
|
||||
return RedirectToAction(nameof(GeneralSettings), new
|
||||
{
|
||||
storeId = CurrentStore.Id
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("{storeId}/delete")]
|
||||
public IActionResult DeleteStore(string storeId)
|
||||
{
|
||||
|
@ -1,12 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Models.StoreViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Rates;
|
||||
@ -37,6 +35,26 @@ namespace BTCPayServer.Controllers
|
||||
_rateFactory = rateFactory;
|
||||
}
|
||||
|
||||
[HttpGet()]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
|
||||
public async Task<IActionResult> ListStores(bool archived = false)
|
||||
{
|
||||
var stores = await _repo.GetStoresByUserId(GetUserId());
|
||||
var vm = new ListStoresViewModel
|
||||
{
|
||||
Stores = stores
|
||||
.Where(s => s.Archived == archived)
|
||||
.Select(s => new ListStoresViewModel.StoreViewModel
|
||||
{
|
||||
StoreId = s.Id,
|
||||
StoreName = s.StoreName,
|
||||
Archived = s.Archived
|
||||
}).ToList(),
|
||||
Archived = archived
|
||||
};
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("create")]
|
||||
[Authorize(AuthenticationSchemes = AuthenticationSchemes.Cookie, Policy = Policies.CanModifyStoreSettingsUnscoped)]
|
||||
public async Task<IActionResult> CreateStore(bool skipWizard)
|
||||
|
@ -1381,9 +1381,9 @@ namespace BTCPayServer.Controllers
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("{walletId}/labels")]
|
||||
[HttpGet("{walletId}/labels.json")]
|
||||
[IgnoreAntiforgeryToken]
|
||||
public async Task<IActionResult> GetLabels(
|
||||
public async Task<IActionResult> LabelsJson(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId,
|
||||
bool excludeTypes,
|
||||
string? type = null,
|
||||
@ -1397,14 +1397,62 @@ namespace BTCPayServer.Controllers
|
||||
: await WalletRepository.GetWalletLabels(walletObjectId);
|
||||
return Ok(labels
|
||||
.Where(l => !excludeTypes || !WalletObjectData.Types.AllTypes.Contains(l.Label))
|
||||
.Select(tuple => new
|
||||
.Select(tuple => new WalletLabelModel
|
||||
{
|
||||
label = tuple.Label,
|
||||
color = tuple.Color,
|
||||
textColor = ColorPalette.Default.TextColor(tuple.Color)
|
||||
Label = tuple.Label,
|
||||
Color = tuple.Color,
|
||||
TextColor = ColorPalette.Default.TextColor(tuple.Color)
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpGet("{walletId}/labels")]
|
||||
public async Task<IActionResult> WalletLabels(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId)
|
||||
{
|
||||
if (walletId.StoreId == null)
|
||||
return NotFound();
|
||||
|
||||
var labels = await WalletRepository.GetWalletLabels(walletId);
|
||||
|
||||
var vm = new WalletLabelsModel
|
||||
{
|
||||
WalletId = walletId,
|
||||
Labels = labels
|
||||
.Where(l => !WalletObjectData.Types.AllTypes.Contains(l.Label))
|
||||
.Select(tuple => new WalletLabelModel
|
||||
{
|
||||
Label = tuple.Label,
|
||||
Color = tuple.Color,
|
||||
TextColor = ColorPalette.Default.TextColor(tuple.Color)
|
||||
})
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpPost("{walletId}/labels/{id}/remove")]
|
||||
public async Task<IActionResult> RemoveWalletLabel(
|
||||
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||
WalletId walletId, string id)
|
||||
{
|
||||
if (walletId.StoreId == null)
|
||||
return NotFound();
|
||||
|
||||
var labels = new[] { id };
|
||||
;
|
||||
if (await WalletRepository.RemoveWalletLabels(walletId, labels))
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "The label has been successfully removed.";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "The label could not be removed.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(WalletLabels), new { walletId });
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
||||
|
@ -72,7 +72,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
|
||||
return (await dbContext.Payouts
|
||||
.Include(data => data.PullPaymentData)
|
||||
.ThenInclude(data => data.StoreData)
|
||||
.Include(data => data.StoreData)
|
||||
.ThenInclude(data => data.UserStores)
|
||||
.ThenInclude(data => data.StoreRole)
|
||||
.Where(data =>
|
||||
@ -82,11 +82,11 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
.ToListAsync())
|
||||
.Where(payout =>
|
||||
{
|
||||
if (approvedStores.TryGetValue(payout.PullPaymentData.StoreId, out var value))
|
||||
if (approvedStores.TryGetValue(payout.StoreDataId, out var value))
|
||||
return value;
|
||||
value = payout.PullPaymentData.StoreData.UserStores
|
||||
value = payout.StoreData.UserStores
|
||||
.Any(store => store.ApplicationUserId == userId && store.StoreRole.Permissions.Contains(Policies.CanModifyStoreSettings));
|
||||
approvedStores.Add(payout.PullPaymentData.StoreId, value);
|
||||
approvedStores.Add(payout.StoreDataId, value);
|
||||
return value;
|
||||
}).ToList();
|
||||
}
|
||||
@ -125,7 +125,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
|
||||
await using var ctx = _applicationDbContextFactory.CreateContext();
|
||||
|
||||
var payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.PullPaymentData.StoreId);
|
||||
var payouts = (await GetPayouts(ctx, pmi, payoutIds)).GroupBy(data => data.StoreDataId);
|
||||
var results = new List<ResultVM>();
|
||||
var network = _btcPayNetworkProvider.GetNetwork<BTCPayNetwork>(pmi.CryptoCode);
|
||||
|
||||
@ -134,7 +134,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
|
||||
var authorizedForInternalNode = (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanModifyServerSettings))).Succeeded;
|
||||
foreach (var payoutDatas in payouts)
|
||||
{
|
||||
var store = payoutDatas.First().PullPaymentData.StoreData;
|
||||
var store = payoutDatas.First().StoreData;
|
||||
|
||||
var lightningSupportedPaymentMethod = store.GetSupportedPaymentMethods(_btcPayNetworkProvider)
|
||||
.OfType<LightningSupportedPaymentMethod>()
|
||||
|
@ -33,6 +33,22 @@ namespace BTCPayServer.Data
|
||||
return PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) ? paymentMethodId : null;
|
||||
}
|
||||
|
||||
public static string GetPayoutSource(this PayoutData data, BTCPayNetworkJsonSerializerSettings jsonSerializerSettings)
|
||||
{
|
||||
var ppBlob = data.PullPaymentData?.GetBlob();
|
||||
var payoutBlob = data.GetBlob(jsonSerializerSettings);
|
||||
string payoutSource;
|
||||
if (payoutBlob.Metadata?.TryGetValue("source", StringComparison.InvariantCultureIgnoreCase,
|
||||
out var source) is true)
|
||||
{
|
||||
return source.Value<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
return ppBlob?.Name ?? data.PullPaymentDataId;
|
||||
}
|
||||
}
|
||||
|
||||
public static PayoutBlob GetBlob(this PayoutData data, BTCPayNetworkJsonSerializerSettings serializers)
|
||||
{
|
||||
var result = JsonConvert.DeserializeObject<PayoutBlob>(Encoding.UTF8.GetString(data.Blob), serializers.GetSerializer(data.GetPaymentMethodId().CryptoCode));
|
||||
|
@ -238,7 +238,7 @@ namespace BTCPayServer.Data
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool CelebratePayment { get; set; } = true;
|
||||
|
||||
[DefaultValue(true)]
|
||||
[DefaultValue(false)]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
public bool PlaySoundOnPayment { get; set; } = false;
|
||||
|
||||
|
@ -203,6 +203,9 @@ public class UIFormsController : Controller
|
||||
if (store is null)
|
||||
return NotFound();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
var request = _formDataService.GenerateInvoiceParametersFromForm(form);
|
||||
var inv = await invoiceController.CreateInvoiceCoreRaw(request, store, Request.GetAbsoluteRoot());
|
||||
if (inv.Price == 0 && inv.Type == InvoiceType.Standard && inv.ReceiptOptions?.Enabled is not false)
|
||||
@ -210,5 +213,15 @@ public class UIFormsController : Controller
|
||||
return RedirectToAction("InvoiceReceipt", "UIInvoice", new { invoiceId = inv.Id });
|
||||
}
|
||||
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = inv.Id });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = "Could not generate invoice: "+ e.Message
|
||||
});
|
||||
return await GetFormView(formData, form);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@ -50,45 +51,47 @@ namespace BTCPayServer.HostedServices
|
||||
}
|
||||
}).Where(tuple => tuple.Data != null && tuple.Items.Any(item =>
|
||||
item.Inventory.HasValue &&
|
||||
updateAppInventory.Items.ContainsKey(item.Id)));
|
||||
foreach (var valueTuple in apps)
|
||||
updateAppInventory.Items.FirstOrDefault(i => i.Id == item.Id) != null));
|
||||
foreach (var app in apps)
|
||||
{
|
||||
foreach (var item1 in valueTuple.Items.Where(item =>
|
||||
updateAppInventory.Items.ContainsKey(item.Id)))
|
||||
foreach (var cartItem in updateAppInventory.Items)
|
||||
{
|
||||
var item = app.Items.FirstOrDefault(item => item.Id == cartItem.Id);
|
||||
if (item == null) continue;
|
||||
|
||||
if (updateAppInventory.Deduct)
|
||||
{
|
||||
item1.Inventory -= updateAppInventory.Items[item1.Id];
|
||||
item.Inventory -= cartItem.Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
item1.Inventory += updateAppInventory.Items[item1.Id];
|
||||
item.Inventory += cartItem.Count;
|
||||
}
|
||||
}
|
||||
|
||||
switch (valueTuple.Data.AppType)
|
||||
switch (app.Data.AppType)
|
||||
{
|
||||
case PointOfSaleAppType.AppType:
|
||||
((PointOfSaleSettings)valueTuple.Settings).Template =
|
||||
AppService.SerializeTemplate(valueTuple.Items);
|
||||
((PointOfSaleSettings)app.Settings).Template =
|
||||
AppService.SerializeTemplate(app.Items);
|
||||
break;
|
||||
case CrowdfundAppType.AppType:
|
||||
((CrowdfundSettings)valueTuple.Settings).PerksTemplate =
|
||||
AppService.SerializeTemplate(valueTuple.Items);
|
||||
((CrowdfundSettings)app.Settings).PerksTemplate =
|
||||
AppService.SerializeTemplate(app.Items);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
valueTuple.Data.SetSettings(valueTuple.Settings);
|
||||
await _appService.UpdateOrCreateApp(valueTuple.Data);
|
||||
app.Data.SetSettings(app.Settings);
|
||||
await _appService.UpdateOrCreateApp(app.Data);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
else if (evt is InvoiceEvent invoiceEvent)
|
||||
{
|
||||
Dictionary<string, int> cartItems = null;
|
||||
List<PosCartItem> cartItems = null;
|
||||
bool deduct;
|
||||
switch (invoiceEvent.Name)
|
||||
{
|
||||
@ -104,8 +107,8 @@ namespace BTCPayServer.HostedServices
|
||||
return;
|
||||
}
|
||||
|
||||
if ((!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
|
||||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems)))
|
||||
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode) ||
|
||||
AppService.TryParsePosCartItems(invoiceEvent.Invoice.Metadata.PosData, out cartItems))
|
||||
{
|
||||
var appIds = AppService.GetAppInternalTags(invoiceEvent.Invoice);
|
||||
|
||||
@ -114,13 +117,18 @@ namespace BTCPayServer.HostedServices
|
||||
return;
|
||||
}
|
||||
|
||||
var items = cartItems ?? new Dictionary<string, int>();
|
||||
var items = cartItems?.ToList() ?? new List<PosCartItem>();
|
||||
if (!string.IsNullOrEmpty(invoiceEvent.Invoice.Metadata.ItemCode))
|
||||
{
|
||||
items.TryAdd(invoiceEvent.Invoice.Metadata.ItemCode, 1);
|
||||
items.Add(new PosCartItem
|
||||
{
|
||||
Id = invoiceEvent.Invoice.Metadata.ItemCode,
|
||||
Count = 1,
|
||||
Price = invoiceEvent.Invoice.Price
|
||||
});
|
||||
}
|
||||
|
||||
_eventAggregator.Publish(new UpdateAppInventory()
|
||||
_eventAggregator.Publish(new UpdateAppInventory
|
||||
{
|
||||
Deduct = deduct,
|
||||
Items = items,
|
||||
@ -134,7 +142,7 @@ namespace BTCPayServer.HostedServices
|
||||
public class UpdateAppInventory
|
||||
{
|
||||
public string[] AppId { get; set; }
|
||||
public Dictionary<string, int> Items { get; set; }
|
||||
public List<PosCartItem> Items { get; set; }
|
||||
public bool Deduct { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
|
@ -157,6 +157,8 @@ namespace BTCPayServer.HostedServices
|
||||
public bool IncludeArchived { get; set; }
|
||||
public bool IncludeStoreData { get; set; }
|
||||
public bool IncludePullPaymentData { get; set; }
|
||||
public DateTimeOffset? From { get; set; }
|
||||
public DateTimeOffset? To { get; set; }
|
||||
}
|
||||
|
||||
public async Task<List<PayoutData>> GetPayouts(PayoutQuery payoutQuery)
|
||||
@ -217,6 +219,14 @@ namespace BTCPayServer.HostedServices
|
||||
data.PullPaymentData == null || !data.PullPaymentData.Archived);
|
||||
}
|
||||
|
||||
if (payoutQuery.From is not null)
|
||||
{
|
||||
query = query.Where(data => data.Date >= payoutQuery.From);
|
||||
}
|
||||
if (payoutQuery.To is not null)
|
||||
{
|
||||
query = query.Where(data => data.Date <= payoutQuery.To);
|
||||
}
|
||||
return await query.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
@ -359,6 +359,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddReportProvider<PaymentsReportProvider>();
|
||||
services.AddReportProvider<OnChainWalletReportProvider>();
|
||||
services.AddReportProvider<ProductsReportProvider>();
|
||||
services.AddReportProvider<PayoutsReportProvider>();
|
||||
|
||||
services.AddHttpClient(WebhookSender.OnionNamedClient)
|
||||
.ConfigurePrimaryHttpMessageHandler<Socks5HttpClientHandler>();
|
||||
@ -422,9 +423,7 @@ namespace BTCPayServer.Hosting
|
||||
services.AddSingleton<INotificationHandler, PayoutNotification.Handler>();
|
||||
services.AddSingleton<INotificationHandler, ExternalPayoutTransactionNotification.Handler>();
|
||||
services.AddSingleton<IHostedService, DbMigrationsHostedService>();
|
||||
#if DEBUG
|
||||
services.AddSingleton<INotificationHandler, JunkNotification.Handler>();
|
||||
#endif
|
||||
|
||||
services.TryAddSingleton<ExplorerClientProvider>();
|
||||
services.AddSingleton<IExplorerClientProvider, ExplorerClientProvider>(x =>
|
||||
x.GetRequiredService<ExplorerClientProvider>());
|
||||
|
@ -25,6 +25,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Rewrite;
|
||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@ -56,7 +57,6 @@ namespace BTCPayServer.Hosting
|
||||
}
|
||||
public ILoggerFactory LoggerFactory { get; }
|
||||
public Logs Logs { get; }
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddMemoryCache();
|
||||
@ -146,6 +146,8 @@ namespace BTCPayServer.Hosting
|
||||
.AddPlugins(services, Configuration, LoggerFactory)
|
||||
.AddControllersAsServices();
|
||||
|
||||
services.AddServerSideBlazor();
|
||||
|
||||
LowercaseTransformer.Register(services);
|
||||
ValidateControllerNameTransformer.Register(services);
|
||||
|
||||
@ -248,6 +250,13 @@ namespace BTCPayServer.Hosting
|
||||
rateLimits.SetZone($"zone={ZoneLimits.ForgotPassword} rate=5r/d burst=5 nodelay");
|
||||
}
|
||||
|
||||
// HACK: blazor server js hard code some path, making it works only on root path. This fix it.
|
||||
// Workaround this bug https://github.com/dotnet/aspnetcore/issues/43191
|
||||
var rewriteOptions = new RewriteOptions();
|
||||
rewriteOptions.AddRewrite("_blazor/(negotiate|initializers|disconnect)$", "/_blazor/$1", skipRemainingRules: true);
|
||||
rewriteOptions.AddRewrite("_blazor$", "/_blazor", skipRemainingRules: true);
|
||||
app.UseRewriter(rewriteOptions);
|
||||
|
||||
app.UseHeadersOverride();
|
||||
var forwardingOptions = new ForwardedHeadersOptions()
|
||||
{
|
||||
@ -264,15 +273,18 @@ namespace BTCPayServer.Hosting
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
|
||||
|
||||
// HACK: Make blazor js available on: ~/_blazorfiles/_framework/blazor.server.js
|
||||
// Workaround this bug https://github.com/dotnet/aspnetcore/issues/19578
|
||||
app.UseStaticFiles(new StaticFileOptions()
|
||||
{
|
||||
RequestPath = "/_blazorfiles",
|
||||
FileProvider = new ManifestEmbeddedFileProvider(typeof(ComponentServiceCollectionExtensions).Assembly),
|
||||
OnPrepareResponse = LongCache
|
||||
});
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
OnPrepareResponse = ctx =>
|
||||
{
|
||||
// Cache static assets for one year, set asp-append-version="true" on references to update on change.
|
||||
// https://andrewlock.net/adding-cache-control-headers-to-static-files-in-asp-net-core/
|
||||
const int durationInSeconds = 60 * 60 * 24 * 365;
|
||||
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + durationInSeconds;
|
||||
}
|
||||
OnPrepareResponse = LongCache
|
||||
});
|
||||
|
||||
// The framework during publish automatically publish the js files into
|
||||
@ -300,10 +312,12 @@ namespace BTCPayServer.Hosting
|
||||
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
|
||||
Secure = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest
|
||||
});
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
AppHub.Register(endpoints);
|
||||
PaymentRequestHub.Register(endpoints);
|
||||
endpoints.MapBlazorHub().RequireAuthorization();
|
||||
endpoints.MapRazorPages();
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapControllerRoute("default", "{controller:validate=UIHome}/{action:lowercase=Index}/{id?}");
|
||||
@ -311,6 +325,14 @@ namespace BTCPayServer.Hosting
|
||||
app.UsePlugins();
|
||||
}
|
||||
|
||||
private static void LongCache(Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext ctx)
|
||||
{
|
||||
// Cache static assets for one year, set asp-append-version="true" on references to update on change.
|
||||
// https://andrewlock.net/adding-cache-control-headers-to-static-files-in-asp-net-core/
|
||||
const int durationInSeconds = 60 * 60 * 24 * 365;
|
||||
ctx.Context.Response.Headers[HeaderNames.CacheControl] = "public,max-age=" + durationInSeconds;
|
||||
}
|
||||
|
||||
private static Action<Microsoft.AspNetCore.StaticFiles.StaticFileResponseContext> NewMethod()
|
||||
{
|
||||
return ctx =>
|
||||
|
@ -214,7 +214,8 @@ namespace BTCPayServer.Hosting
|
||||
var typeMapping = t.EntityTypeMappings.Single();
|
||||
var query = (IQueryable<object>)otherContext.GetType().GetMethod("Set", new Type[0])!.MakeGenericMethod(typeMapping.EntityType.ClrType).Invoke(otherContext, null)!;
|
||||
if (t.Name == "WebhookDeliveries" ||
|
||||
t.Name == "InvoiceWebhookDeliveries")
|
||||
t.Name == "InvoiceWebhookDeliveries" ||
|
||||
t.Name == "StoreRoles")
|
||||
continue;
|
||||
Logger.LogInformation($"Migrating table: " + t.Name);
|
||||
List<PropertyInfo> datetimeProperties = new List<PropertyInfo>();
|
||||
|
@ -14,7 +14,7 @@ namespace BTCPayServer.Models.AccountViewModels
|
||||
public string Password { get; set; }
|
||||
public string LoginCode { get; set; }
|
||||
|
||||
[Display(Name = "Remember me?")]
|
||||
[Display(Name = "Remember me")]
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ namespace BTCPayServer.Models.AppViewModels
|
||||
public DateTimeOffset Created { get; set; }
|
||||
public AppData App { get; set; }
|
||||
public StoreRepository.StoreRole Role { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
}
|
||||
|
||||
public ListAppViewModel[] Apps { get; set; }
|
||||
|
@ -127,6 +127,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
||||
public List<Data.InvoiceEventData> Events { get; internal set; }
|
||||
public string NotificationEmail { get; internal set; }
|
||||
public Dictionary<string, object> Metadata { get; set; }
|
||||
public Dictionary<string, object> ReceiptData { get; set; }
|
||||
public Dictionary<string, object> AdditionalData { get; set; }
|
||||
public List<PaymentEntity> Payments { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
|
@ -119,6 +119,9 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
|
||||
Status = "Pending";
|
||||
IsPending = true;
|
||||
break;
|
||||
case Client.Models.PaymentRequestData.PaymentRequestStatus.Processing:
|
||||
Status = "Processing";
|
||||
break;
|
||||
case Client.Models.PaymentRequestData.PaymentRequestStatus.Completed:
|
||||
Status = "Settled";
|
||||
break;
|
||||
|
@ -39,6 +39,8 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public bool CanDelete { get; set; }
|
||||
|
||||
public bool Archived { get; set; }
|
||||
|
||||
[Display(Name = "Allow anyone to create invoice")]
|
||||
public bool AnyoneCanCreateInvoice { get; set; }
|
||||
|
||||
|
16
BTCPayServer/Models/StoreViewModels/ListStoresViewModel.cs
Normal file
16
BTCPayServer/Models/StoreViewModels/ListStoresViewModel.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Models.StoreViewModels;
|
||||
|
||||
public class ListStoresViewModel
|
||||
{
|
||||
public class StoreViewModel
|
||||
{
|
||||
public string StoreName { get; set; }
|
||||
public string StoreId { get; set; }
|
||||
public bool Archived { get; set; }
|
||||
}
|
||||
|
||||
public List<StoreViewModel> Stores { get; set; } = new ();
|
||||
public bool Archived { get; set; }
|
||||
}
|
16
BTCPayServer/Models/WalletViewModels/WalletLabelsModel.cs
Normal file
16
BTCPayServer/Models/WalletViewModels/WalletLabelsModel.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Models.WalletViewModels;
|
||||
|
||||
public class WalletLabelsModel
|
||||
{
|
||||
public WalletId WalletId { get; set; }
|
||||
public IEnumerable<WalletLabelModel> Labels { get; set; }
|
||||
}
|
||||
|
||||
public class WalletLabelModel
|
||||
{
|
||||
public string Label { get; set; }
|
||||
public string Color { get; set; }
|
||||
public string TextColor { get; set; }
|
||||
}
|
@ -23,6 +23,7 @@ namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
private readonly UIPaymentRequestController _PaymentRequestController;
|
||||
public const string InvoiceCreated = "InvoiceCreated";
|
||||
public const string InvoiceConfirmed = "InvoiceConfirmed";
|
||||
public const string PaymentReceived = "PaymentReceived";
|
||||
public const string InfoUpdated = "InfoUpdated";
|
||||
public const string InvoiceError = "InvoiceError";
|
||||
@ -128,9 +129,13 @@ namespace BTCPayServer.PaymentRequest
|
||||
private async Task CheckingPendingPayments(CancellationToken cancellationToken)
|
||||
{
|
||||
Logs.PayServer.LogInformation("Starting payment request expiration watcher");
|
||||
var items = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery()
|
||||
var items = await _PaymentRequestRepository.FindPaymentRequests(new PaymentRequestQuery
|
||||
{
|
||||
Status = new[] { Client.Models.PaymentRequestData.PaymentRequestStatus.Pending }
|
||||
Status = new[]
|
||||
{
|
||||
PaymentRequestData.PaymentRequestStatus.Pending,
|
||||
PaymentRequestData.PaymentRequestStatus.Processing
|
||||
}
|
||||
}, cancellationToken);
|
||||
Logs.PayServer.LogInformation($"{items.Length} pending payment requests being checked since last run");
|
||||
await Task.WhenAll(items.Select(i => _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(i))
|
||||
@ -157,7 +162,7 @@ namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
foreach (var paymentId in PaymentRequestRepository.GetPaymentIdsFromInternalTags(invoiceEvent.Invoice))
|
||||
{
|
||||
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment || invoiceEvent.Name == InvoiceEvent.MarkedCompleted || invoiceEvent.Name == InvoiceEvent.MarkedInvalid)
|
||||
if (invoiceEvent.Name is InvoiceEvent.ReceivedPayment or InvoiceEvent.MarkedCompleted or InvoiceEvent.MarkedInvalid)
|
||||
{
|
||||
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentId);
|
||||
var data = invoiceEvent.Payment?.GetCryptoPaymentData();
|
||||
@ -168,10 +173,19 @@ namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
data.GetValue(),
|
||||
invoiceEvent.Payment.Currency,
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType?.ToString()
|
||||
invoiceEvent.Payment.GetPaymentMethodId()?.PaymentType.ToString()
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
else if (invoiceEvent.Name is InvoiceEvent.Completed or InvoiceEvent.Confirmed)
|
||||
{
|
||||
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(paymentId);
|
||||
await _HubContext.Clients.Group(paymentId).SendCoreAsync(PaymentRequestHub.InvoiceConfirmed,
|
||||
new object[]
|
||||
{
|
||||
invoiceEvent.InvoiceId
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
await InfoUpdated(paymentId);
|
||||
}
|
||||
@ -181,10 +195,11 @@ namespace BTCPayServer.PaymentRequest
|
||||
await _PaymentRequestService.UpdatePaymentRequestStateIfNeeded(updated.PaymentRequestId);
|
||||
await InfoUpdated(updated.PaymentRequestId);
|
||||
|
||||
var isPending = updated.Data.Status is
|
||||
PaymentRequestData.PaymentRequestStatus.Pending or
|
||||
PaymentRequestData.PaymentRequestStatus.Processing;
|
||||
var expiry = updated.Data.GetBlob().ExpiryDate;
|
||||
if (updated.Data.Status ==
|
||||
PaymentRequestData.PaymentRequestStatus.Pending &&
|
||||
expiry.HasValue)
|
||||
if (isPending && expiry.HasValue)
|
||||
{
|
||||
QueueExpiryTask(
|
||||
updated.PaymentRequestId,
|
||||
|
@ -10,7 +10,6 @@ using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.PaymentRequests;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using PaymentRequestData = BTCPayServer.Data.PaymentRequestData;
|
||||
|
||||
namespace BTCPayServer.PaymentRequest
|
||||
@ -27,7 +26,6 @@ namespace BTCPayServer.PaymentRequest
|
||||
PaymentRequestRepository paymentRequestRepository,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
InvoiceRepository invoiceRepository,
|
||||
AppService appService,
|
||||
DisplayFormatter displayFormatter,
|
||||
CurrencyNameTable currencies)
|
||||
{
|
||||
@ -62,10 +60,19 @@ namespace BTCPayServer.PaymentRequest
|
||||
{
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(pr.Id);
|
||||
var contributions = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||
var allSettled = contributions.All(i => i.Value.States.All(s => s.IsSettled()));
|
||||
var isPaid = contributions.TotalCurrency >= blob.Amount;
|
||||
|
||||
currentStatus = contributions.TotalCurrency >= blob.Amount
|
||||
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
|
||||
: Client.Models.PaymentRequestData.PaymentRequestStatus.Pending;
|
||||
if (isPaid)
|
||||
{
|
||||
currentStatus = allSettled
|
||||
? Client.Models.PaymentRequestData.PaymentRequestStatus.Completed
|
||||
: Client.Models.PaymentRequestData.PaymentRequestStatus.Processing;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentStatus = Client.Models.PaymentRequestData.PaymentRequestStatus.Pending;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStatus != pr.Status)
|
||||
@ -86,12 +93,11 @@ namespace BTCPayServer.PaymentRequest
|
||||
var blob = pr.GetBlob();
|
||||
|
||||
var invoices = await _PaymentRequestRepository.GetInvoicesForPaymentRequest(id);
|
||||
|
||||
var paymentStats = _invoiceRepository.GetContributionsByPaymentMethodId(blob.Currency, invoices, true);
|
||||
var amountDue = blob.Amount - paymentStats.TotalCurrency;
|
||||
var pendingInvoice = invoices.OrderByDescending(entity => entity.InvoiceTime)
|
||||
.FirstOrDefault(entity => entity.Status == InvoiceStatusLegacy.New);
|
||||
|
||||
|
||||
return new ViewPaymentRequestViewModel(pr)
|
||||
{
|
||||
Archived = pr.Archived,
|
||||
|
@ -5,6 +5,8 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.HostedServices;
|
||||
using BTCPayServer.Logging;
|
||||
@ -414,6 +416,15 @@ namespace BTCPayServer.Payments.Bitcoin
|
||||
if (invoice == null)
|
||||
return null;
|
||||
var paymentMethod = invoice.GetPaymentMethod(wallet.Network, PaymentTypes.BTCLike);
|
||||
var bitcoinPaymentMethod = (Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod)paymentMethod.GetPaymentMethodDetails();
|
||||
if (bitcoinPaymentMethod.NetworkFeeMode == NetworkFeeMode.MultiplePaymentsOnly &&
|
||||
bitcoinPaymentMethod.NextNetworkFee == Money.Zero)
|
||||
{
|
||||
bitcoinPaymentMethod.NextNetworkFee = bitcoinPaymentMethod.NetworkFeeRate.GetFee(100); // assume price for 100 bytes
|
||||
paymentMethod.SetPaymentMethodDetails(bitcoinPaymentMethod);
|
||||
await this._InvoiceRepository.UpdateInvoicePaymentMethod(invoice.Id, paymentMethod);
|
||||
invoice = await _InvoiceRepository.GetInvoice(invoice.Id);
|
||||
}
|
||||
wallet.InvalidateCache(strategy);
|
||||
_Aggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment });
|
||||
return invoice;
|
||||
|
@ -121,9 +121,9 @@ namespace BTCPayServer.Payments.Lightning
|
||||
|
||||
public async Task<NodeInfo[]> GetNodeInfo(LightningSupportedPaymentMethod supportedPaymentMethod, BTCPayNetwork network, InvoiceLogs invoiceLogs, bool? preferOnion = null, bool throws = false)
|
||||
{
|
||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||
throw new PaymentMethodUnavailableException("Full node not available");
|
||||
|
||||
var synced = _Dashboard.IsFullySynched(network.CryptoCode, out var summary);
|
||||
if (supportedPaymentMethod.IsInternalNode && !synced)
|
||||
throw new PaymentMethodUnavailableException("Full node not available");;
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(LightningTimeout);
|
||||
@ -156,13 +156,13 @@ namespace BTCPayServer.Payments.Lightning
|
||||
var nodeInfo = preferOnion != null && info.NodeInfoList.Any(i => i.IsTor == preferOnion)
|
||||
? info.NodeInfoList.Where(i => i.IsTor == preferOnion.Value).ToArray()
|
||||
: info.NodeInfoList.Select(i => i).ToArray();
|
||||
|
||||
|
||||
var blocksGap = summary.Status.ChainHeight - info.BlockHeight;
|
||||
if (blocksGap > 10 && !(isLndHub && info.BlockHeight == 0))
|
||||
{
|
||||
throw new PaymentMethodUnavailableException($"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
throw new PaymentMethodUnavailableException(
|
||||
$"The lightning node is not synched ({blocksGap} blocks left)");
|
||||
}
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
catch (Exception e) when (!throws)
|
||||
|
@ -10,7 +10,6 @@ using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models;
|
||||
using BTCPayServer.Plugins.Crowdfund.Models;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Apps;
|
||||
@ -237,6 +236,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
StoreName = app.StoreData?.StoreName,
|
||||
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.TargetCurrency),
|
||||
AppName = app.Name,
|
||||
Archived = app.Archived,
|
||||
Enabled = settings.Enabled,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StartDate = settings.StartDate,
|
||||
@ -346,6 +346,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
|
||||
}
|
||||
|
||||
app.Name = vm.AppName;
|
||||
app.Archived = vm.Archived;
|
||||
var newSettings = new CrowdfundSettings
|
||||
{
|
||||
Title = vm.Title,
|
||||
|
@ -116,5 +116,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
|
||||
|
||||
// NOTE: Improve validation if needed
|
||||
public bool ModelWithMinimumData => Description != null && Title != null && TargetCurrency != null;
|
||||
|
||||
public bool Archived { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Plugins.PointOfSale.Models;
|
||||
using BTCPayServer.Services.Rates;
|
||||
|
||||
@ -61,8 +58,6 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
|
||||
public DateTime? NextResetDate { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public bool Started => !StartDate.HasValue || DateTime.UtcNow > StartDate;
|
||||
|
||||
public bool Ended => EndDate.HasValue && DateTime.UtcNow > EndDate;
|
||||
|
14
BTCPayServer/Plugins/LightningAddresssResolver.cs
Normal file
14
BTCPayServer/Plugins/LightningAddresssResolver.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using LNURL;
|
||||
|
||||
namespace BTCPayServer.Plugins;
|
||||
|
||||
public class LightningAddressResolver
|
||||
{
|
||||
public string Username { get; set; }
|
||||
public LNURLPayRequest LNURLPayRequest { get; set; }
|
||||
|
||||
public LightningAddressResolver(string username)
|
||||
{
|
||||
Username = username;
|
||||
}
|
||||
}
|
@ -184,7 +184,7 @@ namespace BTCPayServer.Plugins.NFC
|
||||
|
||||
try
|
||||
{
|
||||
var result = await info.SendRequest(bolt11, httpClient);
|
||||
var result = await info.SendRequest(bolt11, httpClient, null, null);
|
||||
if (!string.IsNullOrEmpty(result.Status) && result.Status.Equals("ok", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return Ok(result.Reason);
|
||||
|
@ -164,6 +164,11 @@ namespace BTCPayServer.Plugins
|
||||
|
||||
foreach (var plugin in plugins)
|
||||
{
|
||||
if (plugin.Identifier == "BTCPayServer.Plugins.Prism" && plugin.Version <= new Version("1.1.18"))
|
||||
{
|
||||
logger.LogWarning("Please update your prism plugin, this version is incompatible");
|
||||
continue;
|
||||
}
|
||||
try
|
||||
{
|
||||
logger.LogInformation(
|
||||
|
@ -28,7 +28,6 @@ using BTCPayServer.Services.Rates;
|
||||
using BTCPayServer.Services.Stores;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NBitcoin;
|
||||
@ -147,12 +146,17 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var app = await _appService.GetApp(appId, PointOfSaleAppType.AppType);
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
{
|
||||
|
||||
// not allowing negative tips or discounts
|
||||
if (tip < 0 || discount < 0)
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(choiceKey) && amount <= 0)
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
|
||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||
settings.DefaultView = settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
|
||||
var currentView = viewType ?? settings.DefaultView;
|
||||
@ -166,7 +170,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
decimal? price;
|
||||
Dictionary<string, InvoiceSupportedTransactionCurrency> paymentMethods = null;
|
||||
ViewPointOfSaleViewModel.Item choice = null;
|
||||
Dictionary<string, int> cartItems = null;
|
||||
List<PosCartItem> cartItems = null;
|
||||
ViewPointOfSaleViewModel.Item[] choices = null;
|
||||
if (!string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
@ -203,16 +207,15 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
return NotFound();
|
||||
|
||||
title = settings.Title;
|
||||
//if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
|
||||
// if cart IS enabled and we detect posdata that matches the cart system's, check inventory for the items
|
||||
price = amount;
|
||||
if (currentView == PosViewType.Cart &&
|
||||
AppService.TryParsePosCartItems(jposData, out cartItems))
|
||||
if (currentView == PosViewType.Cart && AppService.TryParsePosCartItems(jposData, out cartItems))
|
||||
{
|
||||
price = 0.0m;
|
||||
choices = AppService.Parse(settings.Template, false);
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
var itemChoice = choices.FirstOrDefault(c => c.Id == cartItem.Key);
|
||||
var itemChoice = choices.FirstOrDefault(item => item.Id == cartItem.Id);
|
||||
if (itemChoice == null)
|
||||
return NotFound();
|
||||
|
||||
@ -220,20 +223,21 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
switch (itemChoice.Inventory)
|
||||
{
|
||||
case int i when i <= 0:
|
||||
case <= 0:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
case int inventory when inventory < cartItem.Value:
|
||||
case { } inventory when inventory < cartItem.Count:
|
||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId });
|
||||
}
|
||||
}
|
||||
|
||||
decimal expectedCartItemPrice = 0;
|
||||
if (itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup)
|
||||
{
|
||||
expectedCartItemPrice = itemChoice.Price ?? 0;
|
||||
}
|
||||
var expectedCartItemPrice = itemChoice.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup
|
||||
? itemChoice.Price ?? 0
|
||||
: 0;
|
||||
|
||||
if (cartItem.Price < expectedCartItemPrice)
|
||||
cartItem.Price = expectedCartItemPrice;
|
||||
|
||||
price += expectedCartItemPrice * cartItem.Value;
|
||||
price += cartItem.Price * cartItem.Count;
|
||||
}
|
||||
if (customAmount is { } c)
|
||||
price += c;
|
||||
@ -310,7 +314,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
{
|
||||
Amount = price,
|
||||
Currency = settings.Currency,
|
||||
Metadata = new InvoiceMetadata()
|
||||
Metadata = new InvoiceMetadata
|
||||
{
|
||||
ItemCode = choice?.Id,
|
||||
ItemDesc = title,
|
||||
@ -342,9 +346,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
var receiptData = new JObject();
|
||||
if (choice is not null)
|
||||
{
|
||||
receiptData = JObject.FromObject(new Dictionary<string, string>()
|
||||
receiptData = JObject.FromObject(new Dictionary<string, string>
|
||||
{
|
||||
{"Title", choice.Title}, {"Description", choice.Description},
|
||||
{"Title", choice.Title},
|
||||
{"Description", choice.Description},
|
||||
});
|
||||
}
|
||||
else if (jposData is not null)
|
||||
@ -353,31 +358,33 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
receiptData = new JObject();
|
||||
if (cartItems is not null && choices is not null)
|
||||
{
|
||||
var selectedChoices = choices.Where(item => cartItems.Keys.Contains(item.Id))
|
||||
var posCartItems = cartItems.ToList();
|
||||
var selectedChoices = choices
|
||||
.Where(item => posCartItems.Any(cartItem => cartItem.Id == item.Id))
|
||||
.ToDictionary(item => item.Id);
|
||||
var cartData = new JObject();
|
||||
foreach (KeyValuePair<string, int> cartItem in cartItems)
|
||||
foreach (PosCartItem cartItem in posCartItems)
|
||||
{
|
||||
if (selectedChoices.TryGetValue(cartItem.Key, out var selectedChoice))
|
||||
{
|
||||
cartData.Add(selectedChoice.Title ?? selectedChoice.Id,
|
||||
$"{(selectedChoice.Price is null ? "Any price" : $"{_displayFormatter.Currency((decimal)selectedChoice.Price.Value, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)}")} x {cartItem.Value} = {(selectedChoice.Price is null ? "Any price" : $"{_displayFormatter.Currency(((decimal)selectedChoice.Price.Value) * cartItem.Value, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)}")}");
|
||||
|
||||
}
|
||||
if (!selectedChoices.TryGetValue(cartItem.Id, out var selectedChoice)) continue;
|
||||
var singlePrice = _displayFormatter.Currency(cartItem.Price, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var totalPrice = _displayFormatter.Currency(cartItem.Price * cartItem.Count, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
var ident = selectedChoice.Title ?? selectedChoice.Id;
|
||||
var key = selectedChoice.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed ? ident : $"{ident} ({singlePrice})";
|
||||
cartData.Add(key, $"{cartItem.Count} x {singlePrice} = {totalPrice}");
|
||||
}
|
||||
receiptData.Add("Cart", cartData);
|
||||
}
|
||||
|
||||
receiptData.Add("Subtotal", _displayFormatter.Currency(appPosData.Subtotal, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
if (appPosData.DiscountAmount > 0)
|
||||
{
|
||||
receiptData.Add("Discount",
|
||||
$"{_displayFormatter.Currency(appPosData.DiscountAmount, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol)} {(appPosData.DiscountPercentage > 0 ? $"({appPosData.DiscountPercentage}%)" : string.Empty)}");
|
||||
var discountFormatted = _displayFormatter.Currency(appPosData.DiscountAmount, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol);
|
||||
receiptData.Add("Discount", appPosData.DiscountPercentage > 0 ? $"{appPosData.DiscountPercentage}% = {discountFormatted}" : discountFormatted);
|
||||
}
|
||||
|
||||
if (appPosData.Tip > 0)
|
||||
{
|
||||
receiptData.Add("Tip", _displayFormatter.Currency(appPosData.Tip, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
}
|
||||
receiptData.Add("Total", _displayFormatter.Currency(appPosData.Total, settings.Currency, DisplayFormatter.CurrencyFormat.Symbol));
|
||||
}
|
||||
entity.Metadata.SetAdditionalData("receiptData", receiptData);
|
||||
|
||||
@ -529,6 +536,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
StoreId = app.StoreDataId,
|
||||
StoreName = app.StoreData?.StoreName,
|
||||
StoreDefaultCurrency = await GetStoreDefaultCurrentIfEmpty(app.StoreDataId, settings.Currency),
|
||||
Archived = app.Archived,
|
||||
AppName = app.Name,
|
||||
Title = settings.Title,
|
||||
DefaultView = settings.DefaultView,
|
||||
@ -616,7 +624,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
return View("PointOfSale/UpdatePointOfSale", vm);
|
||||
}
|
||||
|
||||
var storeBlob = GetCurrentStore().GetStoreBlob();
|
||||
var settings = new PointOfSaleSettings
|
||||
{
|
||||
Title = vm.Title,
|
||||
@ -635,12 +642,12 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
||||
RedirectUrl = vm.RedirectUrl,
|
||||
Description = vm.Description,
|
||||
EmbeddedCSS = vm.EmbeddedCSS,
|
||||
RedirectAutomatically =
|
||||
string.IsNullOrEmpty(vm.RedirectAutomatically) ? null : bool.Parse(vm.RedirectAutomatically)
|
||||
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? null : bool.Parse(vm.RedirectAutomatically),
|
||||
FormId = vm.FormId
|
||||
};
|
||||
|
||||
settings.FormId = vm.FormId;
|
||||
app.Name = vm.AppName;
|
||||
app.Archived = vm.Archived;
|
||||
app.SetSettings(settings);
|
||||
await _appService.UpdateOrCreateApp(app);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "App updated";
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user