Compare commits

...

28 Commits

Author SHA1 Message Date
6d0f9120b8 prep for 1.10.2 2023-06-07 18:02:51 +02:00
aafb4a7f2a Fix stale invoice api for settle invoice
fixes #5049
2023-06-07 17:57:03 +02:00
ae432ff237 Fix: Crash on migation of old instances (Fix #5051) 2023-06-07 10:20:39 +02:00
cdc318c71a Pay Button: Fix circular reference when serializing JSON
When apps were set, the `GetAllApps` included the store data, which led to a circular reference when serializing the JSON. That data isn't necessary here, so we can just drop it before rendering.

Fixes #5038.
2023-06-05 12:35:06 +02:00
94d1cec8a9 Hide Sensitive Info: Fix script location
The script snippet needs to be located outside of the theme if-conditions, otherwise it only works if no custom theme is applied.
2023-06-05 12:34:54 +02:00
c0bc19ea59 Update Changelog 2023-06-02 18:21:57 +09:00
6f07714cd9 Language update 2023-06-02 18:18:10 +09:00
a9d2cac23c bump 1.10.1 2023-06-02 18:15:56 +09:00
693b46126b Bump Bitcoin core to 25.0 (#5032) 2023-06-02 16:41:35 +09:00
bbff9710bf fix cart + form combination bug fixes #5031 2023-06-02 09:34:55 +02:00
358e122775 Fix tests 2023-06-01 22:17:42 +09:00
3818468932 Pluginify on chain wallet setup (#4999)
* Pluginify on chain wallet setup

This PR fixes a few logical points in the wallet setup flow to allow more extensive plugin flexibility; It also fixes an issue when building plugins that requires an Altcoin config profile. Here is an example showcasing the Liquid+ plugin using this to enforce that it is a hot wallet (a requirement it has) and that import to RPC is always set, and a new option that is used to configure the wallet further https://i.imgur.com/pDPQ73v.gif

* Update BTCPayServer/Controllers/UIStoresController.Onchain.cs

* update nbx
2023-06-01 21:18:28 +09:00
3d2554fbe1 Make role name show uneditable when not creating 2023-05-31 15:49:34 +02:00
4309603317 Hide topup items from cart 2023-05-31 15:49:34 +02:00
f733c9ea77 Form Builder: Improve wording
Element -> Field. Something bas and I came across while reviewing the blog post.
2023-05-31 14:57:11 +02:00
775ee01171 fix store role deletion fixes #5027 2023-05-31 13:42:38 +02:00
33ec790137 Changelog 2023-05-31 11:50:10 +09:00
0c575c888c Remove payment requirement for marking expired invoices (#5006)
* Remove payment requirement for marking expired invoices

Allows to manually mark expired invoices, regardless of registered payments. See dennisreimann/btcpayserver-plugin-lnbank#34 for context, in which BTCPay Server sometimes did not register payments that were received to a LNbank wallet (this got fixed in btcpayserver/BTCPayServer.Lightning#129)

* Refactor conditions for better readability
2023-05-31 11:49:01 +09:00
24f7e51e3a Small adjustements 2023-05-31 11:27:03 +09:00
0a0cf97c55 Do not cleanup unreachable stores (#5025) 2023-05-31 11:22:37 +09:00
16b988d097 UI: Only display applicable refund options
Fixes #5019.
2023-05-30 12:51:51 +02:00
5edc0ff6ef UI: Fix visual bug with Hide Sensitive Info
Fixes #5011
2023-05-30 11:20:58 +02:00
375b96e508 UI: Center-align recovery phrase
Fixes #5007.
2023-05-30 11:19:16 +02:00
1e72b12074 UI: Store selector link distinguishes between owner and user
The `IsOwner` property went missing with #4940, so everyone landed on the invoices list when switching stores. This brings back the original behaviour of linking to the Dashboard, if the user has the permission to access it.

Fixes #5015.
2023-05-30 11:18:34 +02:00
4a6d52f78e Fix crowdfund perk support fixes #5013 2023-05-30 10:34:48 +02:00
35dd580e74 Fix cart view and provide better naming for default items fixes #5012 2023-05-30 10:05:31 +02:00
79836ef1de make free invoices from pos redirect to receipt and make receipt reload fast on such case 2023-05-30 10:04:23 +02:00
8cb06f9c6c fix user store reole setting fixes #5010 2023-05-30 09:37:14 +02:00
44 changed files with 216 additions and 162 deletions

View File

@ -12,6 +12,8 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/btcpayserver/btcpayserver</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Configurations>Debug;Release;Altcoins-Debug;Altcoins-Release</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<PropertyGroup>
<Version Condition=" '$(Version)' == '' ">1.7.2</Version>

View File

@ -4,7 +4,7 @@
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="NBXplorer.Client" Version="4.2.3" />
<PackageReference Include="NBXplorer.Client" Version="4.2.5" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="2.0.1" />
</ItemGroup>
<ItemGroup Condition="'$(Altcoins)' != 'true'">

View File

@ -306,6 +306,8 @@ retry:
foreach ((CurrencyPair key, Task<RateResult> value) in result)
{
var rateResult = value.GetAwaiter().GetResult();
if (key.ToString() == "BTG_USD")
continue; // shitcoin not supported by bitfinex anymore
TestLogs.LogInformation($"Testing {key}");
if (brokenShitcoins.Contains(key.ToString()))
continue;

View File

@ -73,7 +73,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:24.1-1
image: btcpayserver/bitcoin:25.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -135,7 +135,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:24.1-1
image: btcpayserver/bitcoin:25.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"

View File

@ -70,7 +70,7 @@ services:
- "sshd_datadir:/root/.ssh"
devlnd:
image: btcpayserver/bitcoin:24.1-1
image: btcpayserver/bitcoin:25.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"
@ -121,7 +121,7 @@ services:
bitcoind:
restart: unless-stopped
image: btcpayserver/bitcoin:24.1-1
image: btcpayserver/bitcoin:25.0
environment:
BITCOIN_NETWORK: regtest
BITCOIN_WALLETDIR: "/data/wallets"

View File

@ -23,15 +23,17 @@
@if (Model.Balance.OffchainBalance != null)
{
<div class="balance">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> in channels
</span>
<div class="d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> in channels
</span>
</div>
<div class="balance-details collapse" id="balanceDetailsOffchain">
@if (Model.Balance.OffchainBalance.Opening != null)
{
<div class="mt-2">
<div class="mt-2 d-flex align-items-baseline gap-1">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Opening" data-sensitive>
@Model.Balance.OffchainBalance.Opening
</span>
@ -42,7 +44,7 @@
}
@if (Model.Balance.OffchainBalance.Local != null)
{
<div class="mt-2">
<div class="mt-2 d-flex align-items-baseline gap-1">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Local" data-sensitive>
@Model.Balance.OffchainBalance.Local
</span>
@ -53,7 +55,7 @@
}
@if (Model.Balance.OffchainBalance.Remote != null)
{
<div class="mt-2">
<div class="mt-2 d-flex align-items-baseline gap-1">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Remote" data-sensitive>
@Model.Balance.OffchainBalance.Remote
</span>
@ -64,7 +66,7 @@
}
@if (Model.Balance.OffchainBalance.Closing != null)
{
<div class="mt-2">
<div class="mt-2 d-flex align-items-baseline gap-1">
<span class="fw-semibold" data-balance="@Model.Balance.OffchainBalance.Closing" data-sensitive>
@Model.Balance.OffchainBalance.Closing
</span>
@ -79,14 +81,16 @@
@if (Model.Balance.OnchainBalance != null)
{
<div class="balance">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> on-chain
</span>
<div class="d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> on-chain
</span>
</div>
<div class="balance-details collapse" id="balanceDetailsOnchain">
@if (Model.Balance.OnchainBalance.Confirmed != null)
{
<div class="mt-2">
<div class="mt-2 d-flex align-items-baseline gap-1">
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Confirmed" data-sensitive>
@Model.Balance.OnchainBalance.Confirmed
</span>
@ -97,7 +101,7 @@
}
@if (Model.Balance.OnchainBalance.Unconfirmed != null)
{
<div class="mt-2">
<div class="mt-2 d-flex align-items-baseline gap-1">
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Unconfirmed" data-sensitive>
@Model.Balance.OnchainBalance.Unconfirmed
</span>
@ -108,7 +112,7 @@
}
@if (Model.Balance.OnchainBalance.Reserved != null)
{
<div class="mt-2">
<div class="mt-2 d-flex align-items-baseline gap-1">
<span class="fw-semibold" data-balance="@Model.Balance.OnchainBalance.Reserved" data-sensitive>
@Model.Balance.OnchainBalance.Reserved
</span>

View File

@ -3,7 +3,6 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@using BTCPayServer.Services
@inject SignInManager<ApplicationUser> SignInManager
@inject BTCPayServerEnvironment Env
@inject IFileService FileService
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
@ -33,8 +32,7 @@
else
{
<a asp-controller="UIStores" asp-action="Dashboard" permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
<a asp-controller="UIInvoice" asp-action="ListInvoices" not-permission="@Policies.CanModifyStoreSettings" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
@if (Model.Options.Any())
{

View File

@ -1,5 +1,6 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
@ -38,12 +39,14 @@ namespace BTCPayServer.Components.StoreSelector
.FirstOrDefault()?
.Network.CryptoCode;
var walletId = cryptoCode != null ? new WalletId(store.Id, cryptoCode) : null;
var role = store.GetStoreRoleOfUser(userId);
return new StoreSelectorOption
{
Text = store.StoreName,
Value = store.Id,
Selected = store.Id == currentStore?.Id,
WalletId = walletId
WalletId = walletId,
IsOwner = role != null && role.Permissions.Contains(Policies.CanModifyStoreSettings)
};
})
.OrderBy(s => s.Text)

View File

@ -17,7 +17,7 @@
<header class="mb-3">
@if (Model.Balance != null)
{
<div class="balance">
<div class="balance d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@Model.Balance" data-sensitive>@Model.Balance</h3>
<span class="text-secondary fw-semibold currency">@Model.CryptoCode</span>
</div>

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Amazon.S3.Transfer;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
@ -167,9 +166,17 @@ namespace BTCPayServer.Controllers
[FromServices] StoreRepository storeRepository,
string role)
{
await storeRepository.SetDefaultRole(role);
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
var resolved = await storeRepository.ResolveStoreRoleId(null, role);
if (resolved is null)
{
TempData[WellKnownTempData.ErrorMessage] = "Role could not be set as default";
}
else
{
await storeRepository.SetDefaultRole(role);
TempData[WellKnownTempData.SuccessMessage] = "Role set default";
}
return RedirectToAction(nameof(ListRoles));
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text;
@ -250,7 +251,7 @@ namespace BTCPayServer.Controllers
CryptoCode = cryptoCode,
Method = method,
SetupRequest = request,
Confirmation = string.IsNullOrEmpty(request.ExistingMnemonic),
Confirmation = !isImport,
Network = network,
Source = isImport ? "SeedImported" : "NBXplorerGenerated",
IsHotWallet = isImport ? request.SavePrivateKeys : method == WalletSetupMethod.HotWallet,
@ -311,7 +312,7 @@ namespace BTCPayServer.Controllers
var result = await UpdateWallet(vm);
if (!ModelState.IsValid || !(result is RedirectToActionResult))
if (!ModelState.IsValid || result is not RedirectToActionResult)
return result;
if (!isImport)

View File

@ -141,7 +141,7 @@ namespace BTCPayServer.Controllers
"Delete"));
}
[HttpPost("{storeId}/roles/{roleId}/delete")]
[HttpPost("{storeId}/roles/{role}/delete")]
public async Task<IActionResult> DeleteRolePost(
string storeId,
[FromServices] StoreRepository storeRepository,

View File

@ -111,12 +111,6 @@ namespace BTCPayServer.Hosting
settings.DeprecatedLightningConnectionStringCheck = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.UnreachableStoreCheck)
{
await UnreachableStoreCheck();
settings.UnreachableStoreCheck = true;
await _Settings.UpdateSetting(settings);
}
if (!settings.ConvertMultiplierToSpread)
{
await ConvertMultiplierToSpread();
@ -657,13 +651,6 @@ WHERE cte.""Id""=p.""Id""
settings1.TargetCurrency = app.StoreData.GetStoreBlob().DefaultCurrency;
app.SetSettings(settings1);
}
items = AppService.Parse(settings1.PerksTemplate);
newTemplate = AppService.SerializeTemplate(items);
if (settings1.PerksTemplate != newTemplate)
{
settings1.PerksTemplate = newTemplate;
app.SetSettings(settings1);
};
break;
case PointOfSaleAppType.AppType:
@ -674,13 +661,6 @@ WHERE cte.""Id""=p.""Id""
settings2.Currency = app.StoreData.GetStoreBlob().DefaultCurrency;
app.SetSettings(settings2);
}
items = AppService.Parse(settings2.Template);
newTemplate = AppService.SerializeTemplate(items);
if (settings2.Template != newTemplate)
{
settings2.Template = newTemplate;
app.SetSettings(settings2);
};
break;
}
}
@ -1068,11 +1048,6 @@ retry:
}
}
private Task UnreachableStoreCheck()
{
return _StoreRepository.CleanUnreachableStores();
}
private async Task DeprecatedLightningConnectionStringCheck()
{
using var ctx = _DBContextFactory.CreateContext();

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using NBXplorer.Models;
namespace BTCPayServer.Models.StoreViewModels

View File

@ -60,6 +60,11 @@ namespace BTCPayServer.Plugins.PayButton.Controllers
}
var apps = await _appService.GetAllApps(_userManager.GetUserId(User), false, store.Id);
// unset app store data, because we don't need it and inclusion leads to circular references when serializing to JSON
foreach (var app in apps)
{
app.App.StoreData = null;
}
var appUrl = HttpContext.Request.GetAbsoluteRoot().WithTrailingSlash();
var model = new PayButtonViewModel
{

View File

@ -240,6 +240,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
}
var store = await _appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
var posFormId = settings.FormId;
var formData = await FormDataService.GetForm(posFormId);
@ -297,7 +298,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
RedirectAutomatically = settings.RedirectAutomatically,
SupportedTransactionCurrencies = paymentMethods,
RequiresRefundEmail = requiresRefundEmail == RequiresRefundEmail.InheritFromStore
? store.GetStoreBlob().RequiresRefundEmail
? storeBlob.RequiresRefundEmail
: requiresRefundEmail == RequiresRefundEmail.On,
}, store, HttpContext.Request.GetAbsoluteRoot(),
new List<string> { AppService.GetAppInternalTag(appId) },
@ -355,6 +356,10 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
meta.Merge(formResponseJObject);
entity.Metadata = InvoiceMetadata.FromJObject(meta);
});
if (price is 0 && storeBlob.ReceiptOptions?.Enabled is true)
{
return RedirectToAction(nameof(UIInvoiceController.InvoiceReceipt), "UIInvoice", new { invoiceId = invoice.Data.Id });
}
return RedirectToAction(nameof(UIInvoiceController.Checkout), "UIInvoice", new { invoiceId = invoice.Data.Id });
}
catch (BitpayHttpException e)

View File

@ -51,7 +51,6 @@ namespace BTCPayServer.Plugins.PointOfSale
private readonly LinkGenerator _linkGenerator;
private readonly IOptions<BTCPayServerOptions> _btcPayServerOptions;
private readonly DisplayFormatter _displayFormatter;
private readonly HtmlSanitizer _htmlSanitizer;
public const string AppType = "PointOfSale";
public PointOfSaleAppType(
@ -65,7 +64,6 @@ namespace BTCPayServer.Plugins.PointOfSale
_linkGenerator = linkGenerator;
_btcPayServerOptions = btcPayServerOptions;
_displayFormatter = displayFormatter;
_htmlSanitizer = htmlSanitizer;
}
public override Task<string> ConfigureLink(AppData app)

View File

@ -176,7 +176,7 @@ namespace BTCPayServer.Services.Apps
res.Add(new InvoiceStatsItem
{
ItemCode = item.Id,
FiatPrice = lineItem.Price.Value,
FiatPrice = lineItem.Price,
Date = e.InvoiceTime.Date
});
}

View File

@ -34,44 +34,44 @@ namespace BTCPayServer.Services.Apps
new()
{
Id = "rooibos",
Title = "Rooibos",
Title = "Rooibos (limited)",
Description =
"Rooibos is a dramatic red tea made from a South African herb that contains polyphenols and flavonoids. Often called 'African redbush tea', Rooibos herbal tea delights the senses and delivers potential health benefits with each caffeine-free sip.",
Image = "~/img/pos-sample/rooibos.jpg",
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed,
Price = 1.2m
Price = 1.2m,
Inventory = 5,
},
new()
{
Id = "pu-erh",
Title = "Pu Erh",
Title = "Pu Erh (free)",
Description =
"This loose pur-erh tea is produced in Yunnan Province, China. The process in a relatively high humidity environment has mellowed the elemental character of the tea when compared to young Pu-erh.",
Image = "~/img/pos-sample/pu-erh.jpg",
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Fixed,
Price = 2
Price = 0
},
new()
{
Id = "herbal-tea",
Title = "Herbal Tea",
Title = "Herbal Tea (minimum)",
Description =
"Chamomile tea is made from the flower heads of the chamomile plant. The medicinal use of chamomile dates back to the ancient Egyptians, Romans and Greeks. Pay us what you want!",
Image = "~/img/pos-sample/herbal-tea.jpg",
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Minimum,
Price = 1.8m,
Disabled = true
Disabled = false
},
new()
{
Id = "fruit-tea",
Title = "Fruit Tea",
Title = "Fruit Tea (any amount)",
Description =
"The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!",
Image = "~/img/pos-sample/fruit-tea.jpg",
PriceType = ViewPointOfSaleViewModel.ItemPriceType.Topup,
Inventory = 5,
Disabled = true
Disabled = false
}
});
DefaultView = PosViewType.Static;

View File

@ -834,20 +834,13 @@ namespace BTCPayServer.Services.Invoices
public bool CanMarkComplete()
{
return (Status == InvoiceStatusLegacy.Paid) ||
(Status == InvoiceStatusLegacy.New) ||
((Status == InvoiceStatusLegacy.New || Status == InvoiceStatusLegacy.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
((Status == InvoiceStatusLegacy.New || Status == InvoiceStatusLegacy.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
(Status != InvoiceStatusLegacy.Complete && ExceptionStatus == InvoiceExceptionStatus.Marked) ||
(Status == InvoiceStatusLegacy.Invalid);
return Status is InvoiceStatusLegacy.New or InvoiceStatusLegacy.Paid or InvoiceStatusLegacy.Expired or InvoiceStatusLegacy.Invalid ||
(Status != InvoiceStatusLegacy.Complete && ExceptionStatus == InvoiceExceptionStatus.Marked);
}
public bool CanMarkInvalid()
{
return (Status == InvoiceStatusLegacy.Paid) ||
(Status == InvoiceStatusLegacy.New) ||
((Status == InvoiceStatusLegacy.New || Status == InvoiceStatusLegacy.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidPartial) ||
((Status == InvoiceStatusLegacy.New || Status == InvoiceStatusLegacy.Expired) && ExceptionStatus == InvoiceExceptionStatus.PaidLate) ||
return Status is InvoiceStatusLegacy.New or InvoiceStatusLegacy.Paid or InvoiceStatusLegacy.Expired ||
(Status != InvoiceStatusLegacy.Invalid && ExceptionStatus == InvoiceExceptionStatus.Marked);
}

View File

@ -521,8 +521,14 @@ namespace BTCPayServer.Services.Invoices
invoiceData.Status = legacyStatus.ToLowerInvariant();
invoiceData.ExceptionStatus = InvoiceExceptionStatus.Marked.ToString().ToLowerInvariant();
_eventAggregator.Publish(new InvoiceEvent(ToEntity(invoiceData), eventName));
await context.SaveChangesAsync();
try
{
await context.SaveChangesAsync();
}
finally
{
_eventAggregator.Publish(new InvoiceEvent(ToEntity(invoiceData), eventName));
}
}
return true;

View File

@ -34,7 +34,7 @@ public class PosAppCartItem
public string Id { get; set; }
[JsonProperty(PropertyName = "price")]
public PosAppCartItemPrice Price { get; set; }
public decimal Price { get; set; }
[JsonProperty(PropertyName = "title")]
public string Title { get; set; }
@ -54,8 +54,6 @@ public class PosAppCartItemPrice
[JsonProperty(PropertyName = "formatted")]
public string Formatted { get; set; }
[JsonProperty(PropertyName = "value")]
public decimal Value { get; set; }
[JsonProperty(PropertyName = "type")]
public ViewPointOfSaleViewModel.ItemPriceType Type { get; set; }

View File

@ -7,7 +7,6 @@ namespace BTCPayServer.Services
[JsonProperty("MigrateHotwalletProperty2")]
public bool MigrateHotwalletProperty { get; set; }
public bool MigrateU2FToFIDO2 { get; set; }
public bool UnreachableStoreCheck { get; set; }
public bool DeprecatedLightningConnectionStringCheck { get; set; }
public bool ConvertMultiplierToSpread { get; set; }
public bool ConvertNetworkFeeProperty { get; set; }

View File

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Amazon.S3;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client;
using BTCPayServer.Data;
@ -229,17 +230,19 @@ namespace BTCPayServer.Services.Stores
/// <param name="storeId"></param>
/// <param name="role"></param>
/// <returns></returns>
public async Task<StoreRoleId?> ResolveStoreRoleId(string storeId, string? role)
public async Task<StoreRoleId?> ResolveStoreRoleId(string? storeId, string? role)
{
if (string.IsNullOrWhiteSpace(role))
return null;
if (role.Contains("::", StringComparison.OrdinalIgnoreCase) || storeId.Contains("::", StringComparison.OrdinalIgnoreCase))
if (storeId?.Contains("::", StringComparison.OrdinalIgnoreCase) is true)
return null;
var roleId = StoreRoleId.Parse(role);
if (roleId.StoreId != null && roleId.StoreId != storeId)
return null;
if ((await GetStoreRole(roleId)) != null)
return roleId;
if (string.IsNullOrEmpty(storeId))
return null;
if (roleId.IsServerRole)
roleId = new StoreRoleId(storeId, role);
if ((await GetStoreRole(roleId)) != null)

View File

@ -157,8 +157,6 @@ namespace BTCPayServer.Services
{
_logger.LogError($"Failed to delete user {user.Id}");
}
await _storeRepository.CleanUnreachableStores();
}

View File

@ -41,7 +41,8 @@
}
else
{
<input asp-for="Role" required="required" class="form-control" readonly />
<input type="hidden" asp-for="Role"/>
<input required="required" class="form-control" disabled value="@Model.Role" />
}
<span asp-validation-for="Role" class="text-danger"></span>
</div>

View File

@ -38,7 +38,7 @@
@Safe.Raw("or more")
}
}
else if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed )
else if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup )
{
@Safe.Raw("Any amount")
}else if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Fixed)

View File

@ -318,15 +318,17 @@
<div class="card-title d-flex justify-content-between" :class="{ 'mb-0': !perk.description }">
<span class="h5" :class="{ 'mb-0': !perk.description }">{{perk.title ? perk.title : perk.id}}</span>
<span class="text-muted">
<template v-if="perk.price && perk.price.value">
{{formatAmount(perk.price.value.noExponents(), srvModel.currencyData.divisibility)}}
{{targetCurrency}}
<template v-if="perk.price.type == 1">or more</template>
</template>
<template v-else-if="perk.price.type === 2 && !perk.price.value">
<template v-if="perk.priceType === 'Fixed' && amount ==0">
Free
</template>
<template v-else-if="perk.price.type === 0 || (!perk.price.value && perk.price.type === 1)">
<template v-else-if="amount">
{{formatAmount(perk.price.noExponents(), srvModel.currencyData.divisibility)}}
{{targetCurrency}}
<template v-if="perk.price.type === 'Minimum'">or more</template>
</template>
<template v-else-if="perk.priceType === 'Topup' || (!amount && perk.priceType === 'Minimum')">
Any amount
</template>
</span>
@ -334,19 +336,18 @@
<p class="card-text overflow-hidden" v-if="perk.description" v-html="perk.description"></p>
<div class="input-group" style="max-width:500px;" v-if="expanded" :id="'perk-form'+ perk.id">
<template v-if="perk.price.type !== 0 && !(perk.price.type === 2 && !perk.price.value)">
<template v-if="perk.priceType !== 'Topup' && !(perk.priceType === 'Fixed' && amount == 0)">
<input
:disabled="!active"
:readonly="perk.price.type !== 1"
:readonly="perk.priceType === 'Fixed'"
class="form-control hide-number-spin"
type="number"
v-model="amount"
:min="perk.price.value"
:min="perk.price"
step="any"
placeholder="Contribution Amount"
required>
<span class="input-group-text" >{{targetCurrency}}</span>
<span class="input-group-text">{{targetCurrency}}</span>
</template>
<button
class="btn btn-primary d-flex align-items-center "

View File

@ -5,6 +5,7 @@
@inject ThemeSettings Theme
@inject IFileService FileService
<script>if (window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true') { document.documentElement.setAttribute('data-hide-sensitive-info', 'true')}</script>
@if (Theme.CustomTheme && !string.IsNullOrEmpty(Theme.CssUri))
{ // legacy customization with CSS URI - keep it for backwards-compatibility
<link href="@Context.Request.GetRelativePathOrAbsolute(Theme.CssUri)" rel="stylesheet" asp-append-version="true" />
@ -25,7 +26,6 @@ else
{
<link href="~/main/themes/default.css" asp-append-version="true" rel="stylesheet" />
<link href="~/main/themes/default-dark.css" asp-append-version="true" rel="stylesheet" id="DarkThemeLinkTag" />
<script>if (window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true') { document.documentElement.setAttribute('data-hide-sensitive-info', 'true')}</script>
<script src="~/js/theme-switch.js" asp-append-version="true"></script>
<noscript><style>.btcpay-theme-switch { display: none !important; }</style></noscript>
}

View File

@ -236,6 +236,10 @@
@for (var index = 0; index < Model.Items.Length; index++)
{
var item = Model.Items[index];
if (item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup)
{
continue;
}
var image = item.Image;
var description = item.Description;
@ -255,7 +259,7 @@
<span class="text-muted small">
@{
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Fixed ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
var buttonText = string.IsNullOrEmpty(item.BuyButtonText) ? item.PriceType == ViewPointOfSaleViewModel.ItemPriceType.Topup ? Model.CustomButtonText : Model.ButtonText : item.BuyButtonText;
if (item.PriceType != ViewPointOfSaleViewModel.ItemPriceType.Topup)
{
var formatted = DisplayFormatter.Currency(item.Price ?? 0, Model.CurrencyCode, DisplayFormatter.CurrencyFormat.Symbol);

View File

@ -116,7 +116,7 @@
</div>
<button type="button" class="btn btn-link py-0 px-2 mt-2 mb-2 gap-1 add fw-semibold d-inline-flex align-items-center" v-on:click.stop="$emit('add-field', $event, path)">
<vc:icon symbol="new" />
Add Form Element
Add Form Field
</button>
</div>
</template>

View File

@ -43,7 +43,7 @@
Write them down on a piece of paper in the exact order:
</p>
</div>
<ol id="RecoveryPhrase" data-mnemonic="@Model.Mnemonic" class="my-5 mx-auto">
<ol id="RecoveryPhrase" data-mnemonic="@Model.Mnemonic" class="d-inline-block my-5 mx-auto ps-4">
@foreach (var word in Model.Words)
{
<li class="text-start text-muted py-2">

View File

@ -12,6 +12,7 @@
Layout = null;
ViewData["Title"] = $"Receipt from {Model.StoreName}";
var isProcessing = Model.Status == InvoiceStatus.Processing;
var isFreeInvoice = (Model.Status == InvoiceStatus.New && Model.Amount == 0);
var isSettled = Model.Status == InvoiceStatus.Settled;
}
<!DOCTYPE html>
@ -22,8 +23,14 @@
<meta name="robots" content="noindex,nofollow">
@if (isProcessing)
{
<script type="text/javascript">
setTimeout(() => { window.location.reload(); }, 10000);
<script type="text/javascript">
setTimeout(() => { window.location.reload(); }, 10000);
</script>
}
else if (isFreeInvoice)
{
<script type="text/javascript">
setTimeout(() => { window.location.reload(); }, 2000);
</script>
}
<style>

View File

@ -62,27 +62,36 @@
</div>
<hr class="border" />
}
<div class="form-group">
<div class="form-check">
<input id="RateThenOption" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input"/>
<label for="RateThenOption" class="form-check-label">@Model.RateThenText</label>
<div class="form-text">The crypto currency price, at the rate the invoice got paid.</div>
@if (Model.CryptoAmountThen > 0)
{
<div class="form-group">
<div class="form-check">
<input id="RateThenOption" asp-for="SelectedRefundOption" type="radio" value="RateThen" class="form-check-input" />
<label for="RateThenOption" class="form-check-label">@Model.RateThenText</label>
<div class="form-text">The crypto currency price, at the rate the invoice got paid.</div>
</div>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input id="CurrentRateOption" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input"/>
<label for="CurrentRateOption" class="form-check-label">@Model.CurrentRateText</label>
<div class="form-text">The crypto currency price, at the current rate.</div>
}
@if (Model.CryptoAmountNow > 0)
{
<div class="form-group">
<div class="form-check">
<input id="CurrentRateOption" asp-for="SelectedRefundOption" type="radio" value="CurrentRate" class="form-check-input" />
<label for="CurrentRateOption" class="form-check-label">@Model.CurrentRateText</label>
<div class="form-text">The crypto currency price, at the current rate.</div>
</div>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input id="FiatOption" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input"/>
<label for="FiatOption" class="form-check-label">@Model.FiatText</label>
<div class="form-text">The invoice currency, at the rate when the refund will be sent.</div>
}
@if (Model.FiatAmount > 0)
{
<div class="form-group">
<div class="form-check">
<input id="FiatOption" asp-for="SelectedRefundOption" type="radio" value="Fiat" class="form-check-input" />
<label for="FiatOption" class="form-check-label">@Model.FiatText</label>
<div class="form-text">The invoice currency, at the rate when the refund will be sent.</div>
</div>
</div>
</div>
}
<div class="form-group">
<div class="form-check">
<input id="CustomOption" asp-for="SelectedRefundOption" type="radio" value="Custom" class="form-check-input"/>

View File

@ -140,7 +140,13 @@
</div>
</div>
</div>
@if (Model.AdditionalOptions is not null)
{
@foreach (var dictKeys in Model.AdditionalOptions)
{
<input type="hidden" asp-for="AdditionalOptions[dictKeys.Key]" />
}
}
<button type="submit" class="btn btn-primary" id="Continue">@(isImport ? "Continue" : "Create")</button>
</form>

View File

@ -19,3 +19,5 @@
}
@RenderBody()
<vc:ui-extension-point location="onchain-wallet-setup-post-body" model="@Model"/>

View File

@ -112,7 +112,7 @@ Cart.prototype.getTotalProducts = function() {
typeof this.content[key] != 'undefined' &&
!this.content[key].disabled
) {
const price = this.toCents(this.content[key].price.value ||0);
const price = this.toCents(this.content[key].price ||0);
amount += (this.content[key].count * price);
}
}
@ -438,7 +438,7 @@ Cart.prototype.listItems = function() {
'title': this.escape(item.title),
'count': this.escape(item.count),
'inventory': this.escape(item.inventory < 0? 99999: item.inventory),
'price': this.escape(item.price.formatted || 0)
'price': this.escape(item.price || 0)
});
list.push($(tableTemplate));
}

View File

@ -50,19 +50,22 @@ document.addEventListener("DOMContentLoaded",function (ev) {
}
},
setAmount: function (amount) {
this.amount = this.perk.price.type === 0? null : (amount || 0).noExponents();
if(typeof amount === "string"){
amount = parseFloat(amount);
}
this.amount = this.perk.priceType === "Topup"? null : (amount || 0).noExponents();
this.expanded = false;
}
},
mounted: function () {
this.setAmount(this.perk.price.value);
this.setAmount(this.perk.price);
},
watch: {
perk: function (newValue, oldValue) {
if(newValue.price.type ===0){
if(newValue.price.type === "Topup"){
this.setAmount();
}else if (newValue.price.value != oldValue.price.value) {
this.setAmount(newValue.price.value);
}else if (newValue.price != oldValue.price) {
this.setAmount(newValue.price);
}
}
}
@ -147,9 +150,9 @@ document.addEventListener("DOMContentLoaded",function (ev) {
return result;
},
perks: function(){
var result = [];
for (var i = 0; i < this.srvModel.perks.length; i++) {
var currentPerk = this.srvModel.perks[i];
const result = [];
for (let i = 0; i < this.srvModel.perks.length; i++) {
const currentPerk = this.srvModel.perks[i];
if(this.srvModel.perkCount.hasOwnProperty(currentPerk.id)){
currentPerk.sold = this.srvModel.perkCount[currentPerk.id];
}

View File

@ -1,5 +1,5 @@
Number.prototype.noExponents= function(){
var data= String(this).split(/[eE]/);
String.prototype.noExponents= function(){
const data = String(this).split(/[eE]/);
if(data.length== 1) return data[0];
var z= '', sign= this<0? '-':'',
@ -14,4 +14,8 @@ Number.prototype.noExponents= function(){
mag -= str.length;
while(mag--) z += '0';
return str + z;
}
Number.prototype.noExponents= function(){
return String(this).noExponents();
};

View File

@ -11,20 +11,20 @@
"pay_in_wallet": "Πληρωμή στο πορτοφόλι",
"pay_by_nfc": "Πληρωμή μέσω NFC",
"pay_by_lnurl": "Πληρωμή με LNURL-Withdraw",
"invoice_id": "ID Παραστατικού Πληρωμής",
"order_id": "ID Παραγγελίας",
"invoice_id": "ID παραστατικού πληρωμής",
"order_id": "ID παραγγελίας",
"total_price": "Συνολική τιμή",
"total_fiat": "Συνολική τιμή σε νόμισμα",
"exchange_rate": "Ισοτιμία",
"amount_paid": "Πληρωθέν ποσό",
"amount_due": "Οφειλόμενο ποσό",
"recommended_fee": "Συνιστώμενη Αμοιβή",
"recommended_fee": "Συνιστώμενη αμοιβή",
"fee_rate": "{{feeRate}} sat/byte",
"network_cost": "Κόστος Δικτύου",
"network_cost": "Κόστος δικτύου",
"tx_count": "{{count}} συναλλαγή",
"qr_text": "Σαρώστε τον κωδικό QR ή πατήστε για να αντιγράψετε τη διεύθυνση.",
"address": "Διεύθυνση",
"lightning": "Αστραπή (Lightning)",
"lightning": "Lightning",
"payment_link": "Σύνδεσμος πληρωμής",
"invoice_paid": "Εξοφλημένο τιμολόγιο",
"invoice_expired": "Η τιμολόγηση έληξε",

View File

@ -3,16 +3,16 @@
"code": "el-GR",
"currentLanguage": "Ελληνικά",
"lang": "Γλώσσα",
"Awaiting Payment...": "Αναμονή Πληρωμής...",
"Awaiting Payment...": "Αναμονή πληρωμής...",
"Pay with": "Πληρώστε με",
"Contact and Refund Email": "Email Επικοινωνίας & Επιστροφής Πληρωμής",
"Contact_Body": "Παρακαλούμε εισάγετε το email σας παρακάτω. Θα επικοινωνήσουμε μαζί σας σε αυτή τη διεύθυνση ηλεκτρονικής αλληλογραφίας εαν προκύψει κάποιο θέμα με την πληρωμή σας.",
"Contact and Refund Email": "Email επικοινωνίας & επιστροφής πληρωμής",
"Contact_Body": "Παρακαλούμε εισάγετε το email σας παρακάτω. Θα επικοινωνήσουμε μαζί σας σε αυτή τη διεύθυνση ηλεκτρονικής αλληλογραφίας εάν προκύψει κάποιο θέμα με την πληρωμή σας.",
"Your email": "Το email σας",
"Continue": "Συνέχεια",
"Please enter a valid email address": "Παρακαλούμε εισάγετε μια έγκυρη διεύθυνση email",
"Order Amount": "Ποσό Παραγγελίας",
"Network Cost": "Κόστος Δικτύου",
"Already Paid": "Πληρώθηκαν Ήδη",
"Order Amount": "Ποσό παραγγελίας",
"Network Cost": "Κόστος δικτύου",
"Already Paid": "Πληρώθηκαν ήδη",
"Due": "Οφειλόμενα",
"Scan": "Σάρωση",
"Copy": "Αντιγραφή",
@ -34,14 +34,14 @@
"InvoiceExpired_Body_1": "Το παρών παραστατικό πληρωμής έχει λήξει. Ένα παραστατικό πληρωμής ισχύει μόνο για {{maxTimeMinutes}} λεπτά.\nΜπορείτε να επιστρέψετε στο {{storeName}} εάν θα θέλατε να υποβάλετε ξανά την πληρωμή σας.",
"InvoiceExpired_Body_2": "Εάν επιχειρήσατε να στείλετε την πληρωμή σας, αυτή ακόμη δεν έχει γίνει αποδεκτή απο το δίκτυο. Δέν έχουμε λάβει ακόμη την πληρωμή σας.",
"InvoiceExpired_Body_3": "Εάν την λάβουμε αργότερα, είτε θα εκτέλεσουμε την παραγγελία σας ή θα επικοινωνήσουμε μαζί σας για να οργανώσουμε την επιστροφή των χρημάτων σας...",
"Invoice ID": "ID Παραστατικού Πληρωμής",
"Order ID": "ID Παραγγελίας",
"Invoice ID": "ID παραστατικού πληρωμής",
"Order ID": "ID παραγγελίας",
"Return to StoreName": "Επιστροφή στο {{storeName}}",
"This invoice has been paid": "Αυτό το παραστατικό έχει πληρωθεί",
"This invoice has been archived": "Αυτό το παραστατικό έχει αρχειοθετηθεί",
"Archived_Body": "Παρακαλούμε επικοινωνήστε με το κατάστημα για πληροφορίες σχετικά με την παραγγελία ή εάν χρειάζεστε βοήθεια",
"BOLT 11 Invoice": "Παραστατικό BOLT 11",
"Node Info": "Πληροφορίες Κόμβου",
"Node Info": "Πληροφορίες κόμβου",
"txCount": "{{count}} συναλλαγή",
"txCount_plural": "{{count}} συναλλαγών",
"Pay with CoinSwitch": "Πληρώστε με CoinSwitch",

View File

@ -10,18 +10,17 @@
/* Hide sensitive info */
[data-hide-sensitive-info="true"] [data-sensitive] {
visibility: hidden;
display: inline-flex;
position: relative;
visibility: hidden;
overflow: hidden;
}
[data-hide-sensitive-info="true"] [data-sensitive]:before {
content: '***';
content: '***********************';
visibility: visible;
position: absolute;
}
[data-hide-sensitive-info="true"] .text-end [data-sensitive]:before {
right: 0;
top: .2em;
}
[data-hide-sensitive-info="true"] .store-wallet-balance .ct-label.ct-vertical.ct-start {

View File

@ -1,5 +1,5 @@
<Project>
<PropertyGroup>
<Version>1.10.0</Version>
<Version>1.10.2</Version>
</PropertyGroup>
</Project>

View File

@ -1,5 +1,24 @@
# Changelog
## 1.10.2
### Bug fixes
* Fix: Stale data when fetching invoice after webhook (#5049) @Kukks
* Fix: Crash on migation of old instances (#5051) @NicolasDorier
* Fix: Hide sensitive info feature not working with custom theme (#5044) @dennisreimann
* Fix: Pay button not rendering on the invoice page (#5043) @dennisreimann
## 1.10.1
### Bug fixes
* Point of Sale bug after filling out form Shop + cart (#5031) @Kukks
### Improvements
* Language translation update for el-GR
## 1.10.0
Notice: Due to the substantial disk space consumption, we are removing all data pertaining to past webhook deliveries (#5005).
@ -32,6 +51,7 @@ This data, generally used for debugging integrations, will be regularly purged.
* Improve create first store case (#4951) @dennisreimann @dstrukt
* Improve Refund UI/UX (#4934 #3839 #4812) @dennisreimann @dstrukt
* Prune old webhook delivery data (#5005) @NicolasDorier
* Can mark expired invoices as complete or invalid (#5006) @dennisreimann
## 1.9.3