Compare commits

...

31 Commits

Author SHA1 Message Date
b70c004f29 bump 2024-11-08 16:26:15 +09:00
f6021018cf Fix: Payouts were incorrectly marked as canceled even after successful (#6365)
completion
2024-11-08 16:16:01 +09:00
ce57263cdc improve UX description (#6129)
* Include sparrow 2fa wallet import plus improve UX description

* Add test

* Remove 2FA inclusion

* udate the btcpay network

* 2FA clean up

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
2024-11-08 16:15:42 +09:00
02aec54811 chore: add rider .run folder to .gitignore (#6358)
custom run configurations in rider are stored inside .run folder
2024-11-08 16:15:13 +09:00
ac13cc5363 refactor: use PaymentHash instead of Id when checking pending ln payout (#6360)
Does the same, but the `GetPayment` call explicitly wants a payment
hash, which is clearer this way (thought there might be a bug here when
I first read the code)
2024-11-08 16:15:04 +09:00
829b975c31 chore: rm unnecessary try catch block (#6359)
this one is redundant since the calls to `Pay` and `GetPayment` are
inside their own try catch blocks now
2024-11-08 16:14:52 +09:00
6c8cc811b7 Reports: Fix export (#6357)
Regression from the translation PRs, in which the proper button ID was replaced. Fixes #6356.
2024-11-08 16:14:42 +09:00
8a5a160645 bump 2024-11-04 13:12:35 +09:00
5cbadc09f9 Changelog 2.0.1 2024-11-04 13:08:57 +09:00
7aa87d397e Fix: Wrong manifest downloaded when installing plugin on old btcpay (Fix #6344) (#6354) 2024-11-04 13:05:10 +09:00
693eceb80f Reolve pull payment timezone (#6348) 2024-11-01 08:28:43 +09:00
7d8fc14159 fix: save proof blob if payout is in progress (#6343)
the payout cant be tracked later otherwise and will be marked as
cancelled
2024-11-01 08:24:21 +09:00
4687bb95cb Fix: Incorrect percentage accounting of raised money in crowdfunding (#6347) 2024-11-01 08:23:10 +09:00
e3ec07da76 Fix: Crowdfund page was crashing from 2.0.0 (#6342) (#6346) 2024-10-31 23:42:18 +09:00
910801d305 Replace font-awesome icon on Policies page 2024-10-31 12:23:30 +01:00
5ad0b128aa Dummy commit 2024-10-30 23:39:11 +09:00
5cbeea4fb3 Changelog 2.0 (#6313)
* Changelog 2.0

* Update Changelog.md

---------

Co-authored-by: Andrew Camilleri <evilkukka@gmail.com>
2024-10-30 15:35:00 +01:00
a6e18736d6 Keypad updates (#6338)
* Add keypad icons

Closes #6195.

* Keypad JS fixes
2024-10-29 23:44:37 +09:00
373b90e3b5 Liquid fixes (#6340)
make sure link provider is per payment method of liquid assets. Also remove ETB as it has been unused. Also hide the send button as it is not supported thrrough BTCPay
2024-10-29 23:43:37 +09:00
92f9b226fe Prevent additional concurrency issues with LightnignPendingPayoutListener 2024-10-28 22:12:29 +09:00
0ac6553840 Add download icon 2024-10-28 08:25:30 +01:00
41a2241ae1 feat: log download button (#6330)
* feat: add download button to logs view

* fix: add using block for `fileStream` if it isnt downloaded
2024-10-27 21:43:47 +09:00
9bb1a5b80a Prevent concurrency race on lightning payout update 2024-10-27 19:55:30 +09:00
0e59107eee Fix tests with LightningPendingPayoutListener overriding automated payouts state changes 2024-10-27 19:34:20 +09:00
c9fe68b812 fix: pass current offset to log route (#6329)
the current offset is lost otherwise and will cause a 404 if it was
greater than 0
2024-10-27 19:12:39 +09:00
e7b9688602 refactor: make BitcoinCheckoutModelExtension support other payment handlers (#6311)
* refactor: make `BitcoinCheckoutModelExtension` support other payment handlers

The bitcoin checkout extension doesn't have to be tied to the native
bitcoin handler since it only really needs the payment details to be in
a specific format, which can be provided by other handlers aswell,
allowing for better code reuse.

* refactor: initialize payment methods in constructor
2024-10-25 22:50:46 +09:00
a962e60de9 More Translations (#6318)
* Store selector

* Footer

* Notifications

* Checkout Appearance

* Users list

* Forms

* Emails

* Pay Button

* Edit Dictionary

* Remove newlines, fix typos

* Forms

* Pull payments and payouts

* Various pages

* Use local docs link

* Fix

* Even more translations

* Fixes #6325

* Account pages

* Notifications

* Placeholders

* Various pages and components

* Add more
2024-10-25 22:48:53 +09:00
e5611f9165 Fix tests (#6333) 2024-10-25 22:23:27 +09:00
540ad13265 Paging improvements (#6332)
* Domain Mapping: Passthrough query params when redirecting

* Clean up Pager

* Use current URL when paging

* Refactor
2024-10-25 22:23:03 +09:00
2849426092 Checkout: Allow breaking long item description texts 2024-10-25 13:11:26 +02:00
c4a2b4e975 Merge pull request #6327 from btcpayserver/bugfix/vaulticons
Properly cleaning up old feedback in vault feedback items
2024-10-24 15:33:44 -05:00
207 changed files with 1613 additions and 1202 deletions

1
.gitignore vendored
View File

@ -266,6 +266,7 @@ paket-files/
# JetBrains Rider
.idea/
*.sln.iml
.run
# CodeRush
.cr/

View File

@ -63,7 +63,6 @@ namespace BTCPayServer.Tests
//no tether on our regtest, lets create it and set it
var tether = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT");
var lbtc = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("LBTC");
var etb = tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("ETB");
var issueAssetResult = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
tether.AssetId = uint256.Parse(issueAssetResult.Result["asset"].ToString());
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network)
@ -71,15 +70,10 @@ namespace BTCPayServer.Tests
Assert.Equal(tether.AssetId, tester.NetworkProvider.GetNetwork<ElementsBTCPayNetwork>("USDT").AssetId);
Assert.Equal(tether.AssetId, ((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("USDT").Network).AssetId);
var issueAssetResult2 = await tester.LBTCExplorerNode.SendCommandAsync("issueasset", 100000, 0);
etb.AssetId = uint256.Parse(issueAssetResult2.Result["asset"].ToString());
((ElementsBTCPayNetwork)tester.PayTester.GetService<BTCPayWalletProvider>().GetWallet("ETB").Network)
.AssetId = etb.AssetId;
user.RegisterDerivationScheme("LBTC");
user.RegisterDerivationScheme("USDT");
user.RegisterDerivationScheme("ETB");
//test: register 2 assets on the same elements network and make sure paying an invoice on one does not affect the other in any way
var invoice = await user.BitPay.CreateInvoiceAsync(new Invoice(0.1m, "BTC"));
@ -109,11 +103,7 @@ namespace BTCPayServer.Tests
Assert.Equal("paid", localInvoice.Status);
Assert.Single(localInvoice.CryptoInfo.Single(info => info.CryptoCode.Equals("USDT", StringComparison.InvariantCultureIgnoreCase)).Payments);
});
//test precision based on https://github.com/ElementsProject/elements/issues/805#issuecomment-601277606
var etbBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "ETB").PaymentUrls.BIP21, etb.NBitcoinNetwork);
//precision = 2, 1ETB = 0.00000100
Assert.Equal(100, etbBip21.Amount.Satoshi);
var lbtcBip21 = new BitcoinUrlBuilder(invoice.CryptoInfo.Single(info => info.CryptoCode == "LBTC").PaymentUrls.BIP21, lbtc.NBitcoinNetwork);
//precision = 8, 0.1 = 0.1

View File

@ -2981,9 +2981,10 @@ namespace BTCPayServer.Tests
// check list for store with paid invoice
var merchantInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC");
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
Assert.NotEmpty(merchantInvoices);
Assert.Empty(merchantPendingInvoices);
merchantPendingInvoices = await merchantClient.GetLightningInvoices(merchant.StoreId, "BTC", true);
Assert.True(merchantPendingInvoices.Length < merchantInvoices.Length);
Assert.All(merchantPendingInvoices, m => Assert.Equal(LightningInvoiceStatus.Unpaid, m.Status));
// if the test ran too many times the invoice might be on a later page
if (merchantInvoices.Length < 100)
Assert.Contains(merchantInvoices, i => i.Id == merchantInvoice.Id);
@ -3047,7 +3048,7 @@ namespace BTCPayServer.Tests
new CreateInvoiceRequest
{
Currency = "USD",
Amount = 100,
Amount = 0.1m,
Checkout = new CreateInvoiceRequest.CheckoutOptions
{
PaymentMethods = new[] { "BTC-LN" },

View File

@ -360,7 +360,9 @@ retry:
{
await tester.StartAsync();
var engine = tester.PayTester.GetService<RazorProjectEngine>();
foreach (var file in soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories))
var files = soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories)
.Union(soldir.EnumerateFiles("*.razor", SearchOption.AllDirectories));
foreach (var file in files)
{
var filePath = file.FullName;
var txt = File.ReadAllText(file.FullName);

View File

@ -41,7 +41,7 @@
<PackageReference Include="YamlDotNet" Version="8.0.0" />
<PackageReference Include="BIP78.Sender" Version="0.2.2" />
<PackageReference Include="BTCPayServer.Hwi" Version="2.0.2" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.5" />
<PackageReference Include="BTCPayServer.Lightning.All" Version="1.6.6" />
<PackageReference Include="CsvHelper" Version="32.0.3" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Fido2" Version="2.0.2" />

View File

@ -4,10 +4,12 @@
@using BTCPayServer.Services.Notifications;
@using Microsoft.AspNetCore.Identity;
@using Microsoft.AspNetCore.Routing;
@using Microsoft.Extensions.Localization
@implements IDisposable
@inject AuthenticationStateProvider _AuthenticationStateProvider
@inject NotificationManager _NotificationManager
@inject UserManager<ApplicationUser> _UserManager
@inject IStringLocalizer StringLocalizer
@inject IJSRuntime _JSRuntime
@inject LinkGenerator _LinkGenerator
@inject BTCPayServerOptions _BTCPayServerOptions
@ -16,13 +18,13 @@
<div id="Notifications">
@if (UnseenCount == "0")
{
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="Notifications">
<a href="@NotificationsUrl" id="NotificationsHandle" class="mainMenuButton" title="@StringLocalizer["Notifications"]">
<Icon Symbol="nav-notifications" />
</a>
}
else
{
<button id="NotificationsHandle" class="mainMenuButton" title="Notifications" type="button" data-bs-toggle="dropdown">
<button id="NotificationsHandle" class="mainMenuButton" title="@StringLocalizer["Notifications"]" type="button" data-bs-toggle="dropdown">
<Icon Symbol="nav-notifications" />
<span class="badge rounded-pill bg-danger p-1 ms-1" id="NotificationsBadge">@UnseenCount</span>
</button>
@ -31,8 +33,8 @@
{
<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>
<h5 class="m-0" text-translate="true">Notifications</h5>
<a class="btn btn-link p-0" @onclick="MarkAllAsSeen" id="NotificationsMarkAllAsSeen" text-translate="true">Mark all as seen</a>
</div>
<div id="NotificationsList" v-pre>
@foreach (var n in Last5)
@ -54,7 +56,7 @@
</div>
<div class="p-3">
<a href="@NotificationsUrl">View all</a>
<a href="@NotificationsUrl" text-translate="true">View all</a>
</div>
</div>
}

View File

@ -1,13 +1,13 @@
@using Microsoft.AspNetCore.Http
@inject IHttpContextAccessor HttpContextAccessor;
@inject IHttpContextAccessor HttpContextAccessor
@if (Users?.Any() is true)
{
<div @attributes="Attrs" class="@CssClass">
<label for="SignedInUser" class="form-label">Signed in user</label>
<label for="SignedInUser" class="form-label" text-translate="true">Signed in user</label>
<select id="SignedInUser" class="form-select" value="@_userId" @onchange="@(e => _userId = e.Value?.ToString())">
<option value="">None, just open the URL</option>
<option value="" text-translate="true">None, just open the URL</option>
@foreach (var u in Users)
{
<option value="@u.Key">@u.Value</option>

View File

@ -5,11 +5,13 @@
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Mvc
@using Microsoft.AspNetCore.Routing
@using Microsoft.Extensions.Localization
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject UserManager<ApplicationUser> UserManager;
@inject UserLoginCodeService UserLoginCodeService;
@inject LinkGenerator LinkGenerator;
@inject IHttpContextAccessor HttpContextAccessor;
@inject UserManager<ApplicationUser> UserManager
@inject UserLoginCodeService UserLoginCodeService
@inject LinkGenerator LinkGenerator
@inject IHttpContextAccessor HttpContextAccessor
@inject IStringLocalizer StringLocalizer
@implements IDisposable
@if (!string.IsNullOrEmpty(_data))
@ -18,7 +20,7 @@
<div class="qr-container mb-2">
<QrCode Data="@_data" Size="Size"/>
</div>
<p class="text-center text-muted mb-1" id="progress">Valid for @_seconds seconds</p>
<p class="text-center text-muted mb-1" id="progress">@StringLocalizer["Valid for {0} seconds", _seconds]</p>
<div class="progress only-for-js" data-bs-toggle="tooltip" data-bs-placement="top">
<div class="progress-bar progress-bar-striped progress-bar-animated @(Percent < 15 ? "bg-warning" : null)" role="progressbar" style="width:@Percent%" id="progressbar"></div>
</div>

View File

@ -106,9 +106,14 @@
</li>
@if (ViewData.IsCategoryActive(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsPageActive([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsPageActive([StoreNavPages.OnchainSettings], categoryId))
{
<li class="nav-item nav-item-sub">
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId" text-translate="true">Send</a>
</li>
@if (!scheme.ReadonlyWallet)
{
<li class="nav-item nav-item-sub">
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId" text-translate="true">Send</a>
</li>
}
<li class="nav-item nav-item-sub">
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId" text-translate="true">Receive</a>
</li>

View File

@ -1,3 +1,5 @@
@using System.Web
@using BTCPayServer.TagHelpers
@model BasePagingViewModel
@{
@ -85,10 +87,26 @@
{
// merge both, preferring the `query` properties in case of duplicate keys
query = query.Concat(Model.PaginationQuery)
.Where(e => e.Value != null)
.GroupBy(e => e.Key)
.ToDictionary(g => g.Key, g => g.First().Value);
}
return Url.Action(null, query);
return ReplaceQueryParameters(query);
}
string ReplaceQueryParameters(Dictionary<string, object> query)
{
var uri = new Uri(ViewContext.HttpContext.Request.GetCurrentUrlWithQueryString());
var queryParams = HttpUtility.ParseQueryString(uri.Query);
foreach (var (key, value) in query)
{
if (value != null) queryParams[key] = value?.ToString();
}
var uriBuilder = new UriBuilder(uri)
{
Query = queryParams.ToString()!
};
return uriBuilder.ToString();
}
}

View File

@ -1,7 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Models;
using Microsoft.AspNetCore.Mvc;

View File

@ -17,9 +17,9 @@
<small class="badge bg-warning rounded-pill ms-1 ms-sm-0" title="@type">@displayType</small>
}
}
private static string StoreName(string title)
private string StoreName(string title)
{
return string.IsNullOrEmpty(title) ? "Unnamed Store" : title;
return string.IsNullOrEmpty(title) ? StringLocalizer["Unnamed Store"] : title;
}
#pragma warning restore 1998
}
@ -44,7 +44,7 @@ else
{
<vc:icon symbol="nav-store"/>
}
<span>@(Model.CurrentStoreId == null ? "Select Store" : Model.CurrentDisplayName)</span>
<span>@(Model.CurrentStoreId == null ? StringLocalizer["Select Store"] : Model.CurrentDisplayName)</span>
<vc:icon symbol="caret-down"/>
</button>
<ul id="StoreSelectorMenu" class="dropdown-menu" aria-labelledby="StoreSelectorToggle">
@ -58,15 +58,15 @@ else
{
<li><hr class="dropdown-divider"></li>
}
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Create)" id="StoreSelectorCreate">Create Store</a></li>
<li><a asp-controller="UIUserStores" asp-action="CreateStore" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Create)" id="StoreSelectorCreate" text-translate="true">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.ActivePageClass(StoreNavPages.Index)" id="StoreSelectorArchived">@Model.ArchivedCount Archived Store@(Model.ArchivedCount == 1 ? "" : "s")</a></li>
<li><a asp-controller="UIUserStores" asp-action="ListStores" asp-route-archived="true" class="dropdown-item @ViewData.ActivePageClass(StoreNavPages.Index)" id="StoreSelectorArchived">@(Model.ArchivedCount == 1 ? StringLocalizer["{0} Archived Store", Model.ArchivedCount] : StringLocalizer["{0} Archived Stores", Model.ArchivedCount])</a></li>
}
@*
<li permission="@Policies.CanModifyServerSettings"><hr class="dropdown-divider"></li>
<li permission="@Policies.CanModifyServerSettings"><a asp-controller="UIServer" asp-action="ListStores" class="dropdown-item @ViewData.ActivePageClass(ServerNavPages.Stores)" id="StoreSelectorAdminStores">Admin Store Overview</a></li>
<li permission="@Policies.CanModifyServerSettings"><a asp-controller="UIServer" asp-action="ListStores" class="dropdown-item @ViewData.ActivePageClass(ServerNavPages.Stores)" id="StoreSelectorAdminStores" text-translate="true">Admin Store Overview</a></li>
*@
</ul>
</div>

View File

@ -3,8 +3,8 @@
<div class="btcpay-theme-switch @Model.CssClass">
<span class="btcpay-theme-switch-label" text-translate="true">Theme</span>
<div class="btcpay-theme-switch-themes">
<button type="button" title="System" data-theme="system"><vc:icon symbol="themes-system"/></button>
<button type="button" title="Light" data-theme="light"><vc:icon symbol="themes-light"/></button>
<button type="button" title="Dark" data-theme="dark"><vc:icon symbol="themes-dark"/></button>
<button type="button" title="@StringLocalizer["System"]" data-theme="system"><vc:icon symbol="themes-system"/></button>
<button type="button" title="@StringLocalizer["Light"]" data-theme="light"><vc:icon symbol="themes-light"/></button>
<button type="button" title="@StringLocalizer["Dark"]" data-theme="dark"><vc:icon symbol="themes-dark"/></button>
</div>
</div>

View File

@ -1,27 +1,15 @@
#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;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewComponents;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using NBitcoin;
using NBitcoin.Secp256k1;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Components.WalletNav
{
@ -33,6 +21,7 @@ namespace BTCPayServer.Components.WalletNav
private readonly CurrencyNameTable _currencies;
private readonly DefaultRulesCollection _defaultRules;
private readonly RateFetcher _rateFetcher;
private IStringLocalizer StringLocalizer { get; }
public WalletNav(
BTCPayWalletProvider walletProvider,
@ -40,6 +29,7 @@ namespace BTCPayServer.Components.WalletNav
UIWalletsController walletsController,
CurrencyNameTable currencies,
DefaultRulesCollection defaultRules,
IStringLocalizer stringLocalizer,
RateFetcher rateFetcher)
{
_walletProvider = walletProvider;
@ -48,6 +38,7 @@ namespace BTCPayServer.Components.WalletNav
_currencies = currencies;
_defaultRules = defaultRules;
_rateFetcher = rateFetcher;
StringLocalizer = stringLocalizer;
}
public async Task<IViewComponentResult> InvokeAsync(WalletId walletId)
@ -71,7 +62,7 @@ namespace BTCPayServer.Components.WalletNav
Network = network,
Balance = balance.ShowMoney(network),
DefaultCurrency = defaultCurrency,
Label = derivation?.Label ?? $"{store.StoreName} {walletId.CryptoCode} Wallet"
Label = derivation?.Label ?? $"{store.StoreName} {StringLocalizer["{0} Wallet", walletId.CryptoCode]}"
};
if (defaultCurrency != network.CryptoCode)

View File

@ -9,6 +9,7 @@ using System;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using static BTCPayServer.BoltcardDataExtensions;
namespace BTCPayServer.Controllers
@ -71,7 +72,7 @@ next:
var permission = await vaultClient.AskPermission(VaultServices.NFC, cts.Token);
if (permission is null)
{
await vaultClient.Show(VaultMessageType.Error, "BTCPay Server Vault does not seem to be running, you can download it on <a target=\"_blank\" href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest\">Github</a>.", cts.Token);
await vaultClient.Show(VaultMessageType.Error, StringLocalizer["BTCPay Server Vault does not seem to be running, you can download it on {0}.", new HtmlString("<a href=\"https://github.com/btcpayserver/BTCPayServer.Vault/releases/latest/\" class=\"alert-link\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub</a>")], cts.Token);
goto next;
}
await vaultClient.Show(VaultMessageType.Ok, StringLocalizer["BTCPayServer successfully connected to the vault."], cts.Token);

View File

@ -88,6 +88,9 @@ namespace BTCPayServer.Controllers
foreach (var prop in jobj.Properties())
{
prop.Value = "OK";
if (prop.Name.Contains("{0}")) prop.Value += " {0}";
if (prop.Name.Contains("{1}")) prop.Value += " {1}";
if (prop.Name.Contains("{2}")) prop.Value += " {2}";
}
viewModel.Translations = Translations.CreateFromJson(jobj.ToString()).ToJsonFormat();
}

View File

@ -331,8 +331,8 @@ namespace BTCPayServer.Controllers
}
return View("Confirm", new ConfirmModel(StringLocalizer["Delete admin"],
$"The admin <strong>{Html.Encode(user.Email)}</strong> will be permanently deleted. This action will also delete all accounts, users and data associated with the server account. Are you sure?",
"Delete"));
StringLocalizer["The admin {0} will be permanently deleted. This action will also delete all accounts, users and data associated with the server account. Are you sure?", Html.Encode(user.Email)],
StringLocalizer["Delete"]));
}
return View("Confirm", new ConfirmModel(StringLocalizer["Delete user"], $"The user <strong>{Html.Encode(user.Email)}</strong> will be permanently deleted. Are you sure?", "Delete"));

View File

@ -1275,7 +1275,7 @@ namespace BTCPayServer.Controllers
}
[Route("server/logs/{file?}")]
public async Task<IActionResult> LogsView(string? file = null, int offset = 0)
public async Task<IActionResult> LogsView(string? file = null, int offset = 0, bool download = false)
{
if (offset < 0)
{
@ -1317,13 +1317,23 @@ namespace BTCPayServer.Controllers
return NotFound();
try
{
using var fileStream = new FileStream(
var fileStream = new FileStream(
fi.FullName,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite);
using var reader = new StreamReader(fileStream);
vm.Log = await reader.ReadToEndAsync();
if (download)
{
return new FileStreamResult(fileStream, "text/plain")
{
FileDownloadName = file
};
}
await using (fileStream)
{
using var reader = new StreamReader(fileStream);
vm.Log = await reader.ReadToEndAsync();
}
}
catch
{

View File

@ -146,6 +146,7 @@ public partial class UIStoresController
Crypto = network.CryptoCode,
PaymentMethodId = handler.PaymentMethodId,
WalletSupported = network.WalletSupported,
ReadonlyWallet = network.ReadonlyWallet,
Value = value,
WalletId = new WalletId(store.Id, network.CryptoCode),
Enabled = !excludeFilters.Match(handler.PaymentMethodId) && strategy != null,

View File

@ -192,7 +192,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Destination = blob.Destination,
Message = message
};
@ -216,7 +216,7 @@ namespace BTCPayServer.Data.Payouts.LightningLike
{
public string PayoutId { get; set; }
public string Destination { get; set; }
public PayResult Result { get; set; }
public bool? Success { get; set; }
public string Message { get; set; }
}

View File

@ -46,6 +46,8 @@ namespace BTCPayServer.Filters
var uri = new UriBuilder(req.Scheme, redirectDomain);
if (req.Host.Port.HasValue)
uri.Port = req.Host.Port.Value;
if (req.QueryString.HasValue)
uri.Query = req.QueryString.Value!;
context.RouteContext.HttpContext.Response.Redirect(uri.ToString());
}
return true;

View File

@ -4,6 +4,7 @@ namespace BTCPayServer.Forms;
public class ModifyForm
{
[DisplayName("Name")]
public string Name { get; set; }
[DisplayName("Form configuration (JSON)")]

View File

@ -89,8 +89,7 @@ public class UIFormsController : Controller
if (!_formDataService.IsFormSchemaValid(modifyForm.FormConfig, out var form, out var error))
{
ModelState.AddModelError(nameof(modifyForm.FormConfig),
$"Form config was invalid: {error})");
ModelState.AddModelError(nameof(modifyForm.FormConfig), StringLocalizer["Form config was invalid: {0}", error!]);
}
else
{
@ -117,7 +116,9 @@ public class UIFormsController : Controller
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = $"Form {(isNew ? "created" : "updated")} successfully."
Message = isNew
? StringLocalizer["Form created successfully."].Value
: StringLocalizer["Form updated successfully."].Value
});
if (isNew)
{
@ -126,7 +127,7 @@ public class UIFormsController : Controller
}
catch (Exception e)
{
ModelState.AddModelError("", $"An error occurred while saving: {e.Message}");
ModelState.AddModelError("", StringLocalizer["An error occurred while saving: {0}", e.Message]);
}
return View(modifyForm);
@ -216,14 +217,13 @@ public class UIFormsController : Controller
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)
{
return RedirectToAction("InvoiceReceipt", "UIInvoice", new { invoiceId = inv.Id });
}
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = inv.Id });
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)
{
return RedirectToAction("InvoiceReceipt", "UIInvoice", new { invoiceId = inv.Id });
}
return RedirectToAction("Checkout", "UIInvoice", new { invoiceId = inv.Id });
}
catch (Exception e)
{

View File

@ -13,6 +13,7 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Notifications;
using BTCPayServer.Services.Notifications.Blobs;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
@ -22,15 +23,17 @@ namespace BTCPayServer.HostedServices
{
private const string TYPE = "pluginupdate";
internal class Handler(LinkGenerator linkGenerator, BTCPayServerOptions options) : NotificationHandler<PluginUpdateNotification>
internal class Handler(LinkGenerator linkGenerator, BTCPayServerOptions options, IStringLocalizer stringLocalizer) : NotificationHandler<PluginUpdateNotification>
{
private IStringLocalizer StringLocalizer { get; } = stringLocalizer;
public override string NotificationType => TYPE;
public override (string identifier, string name)[] Meta
{
get
{
return new (string identifier, string name)[] {(TYPE, "Plugin update")};
return new (string identifier, string name)[] {(TYPE, StringLocalizer["Plugin update"])};
}
}
@ -38,7 +41,7 @@ namespace BTCPayServer.HostedServices
{
vm.Identifier = notification.Identifier;
vm.Type = notification.NotificationType;
vm.Body = $"New {notification.Name} plugin version {notification.Version} released!";
vm.Body = StringLocalizer["New {0} plugin version {1} released!", notification.Name, notification.Version];
vm.ActionLink = linkGenerator.GetPathByAction(nameof(UIServerController.ListPlugins),
"UIServer",
new {plugin = notification.PluginIdentifier}, options.RootPath);

View File

@ -1,9 +1,11 @@
using System.ComponentModel.DataAnnotations;
using BTCPayServer.Services;
namespace BTCPayServer.Models.ServerViewModels;
public class EditDictionaryViewModel
{
[Display(Name = "Translations")]
public string Translations { get; set; }
public int Lines { get; set; }
public string Command { get; set; }

View File

@ -107,18 +107,5 @@ namespace BTCPayServer.Models.StoreViewModels
GreaterThan,
LessThan
}
public static string ToString(CriteriaType type)
{
switch (type)
{
case CriteriaType.GreaterThan:
return "Greater than";
case CriteriaType.LessThan:
return "Less than";
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
}
}
}

View File

@ -10,6 +10,7 @@ namespace BTCPayServer.Models.StoreViewModels
[Required]
[MaxLength(50)]
[MinLength(1)]
[Display(Name = "Name")]
public string Name { get; set; }
[Required]

View File

@ -15,6 +15,8 @@ namespace BTCPayServer.Models.StoreViewModels
public string CryptoCode { get; set; }
public bool CanUseInternalNode { get; set; }
public bool SkipPortTest { get; set; }
[Display(Name = "Enabled")]
public bool Enabled { get; set; } = true;
[Display(Name = "Connection string")]

View File

@ -9,6 +9,7 @@ namespace BTCPayServer.Models.StoreViewModels
public string Value { get; set; }
public WalletId WalletId { get; set; }
public bool WalletSupported { get; set; }
public bool ReadonlyWallet { get; set; }
public bool Enabled { get; set; }
public bool Collapsed { get; set; }
}

View File

@ -9,12 +9,15 @@ namespace BTCPayServer.Models.StoreViewModels
public WalletId WalletId { get; set; }
public string StoreId { get; set; }
public bool IsHotWallet { get; set; }
[Display(Name = "Enabled")]
public bool Enabled { get; set; }
public bool CanUsePayJoin { get; set; }
[Display(Name = "Enable Payjoin/P2EP")]
public bool PayJoinEnabled { get; set; }
[Display(Name = "Label")]
public string Label { get; set; }
public string DerivationSchemeInput { get; set; }

View File

@ -43,15 +43,19 @@ namespace BTCPayServer.Models.WalletViewModels
public class NewPullPaymentModel
{
[MaxLength(30)]
[Display(Name = "Name")]
public string Name { get; set; }
[Display(Name = "Description")]
public string Description { get; set; }
[Required]
public decimal Amount
{
get; set;
}
[Display(Name = "Amount")]
public decimal Amount { get; set; }
[Required]
[ReadOnly(true)]
[Display(Name = "Currency")]
public string Currency { get; set; }
[Display(Name = "Payout Methods")]
@ -61,12 +65,12 @@ namespace BTCPayServer.Models.WalletViewModels
[Range(0, 365 * 10)]
public long BOLT11Expiration { get; set; } = 30;
[Display(Name = "Automatically approve claims")]
public bool AutoApproveClaims { get; set; } = false;
public bool AutoApproveClaims { get; set; }
}
public class UpdatePullPaymentModel
{
[Display(Name = "Id")]
public string Id { get; set; }
public UpdatePullPaymentModel()
@ -87,6 +91,7 @@ namespace BTCPayServer.Models.WalletViewModels
}
[MaxLength(30)]
[Display(Name = "Name")]
public string Name { get; set; }
[Display(Name = "Memo")]

View File

@ -7,7 +7,6 @@ using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -16,29 +15,28 @@ namespace BTCPayServer.Payments.Bitcoin
public class BitcoinCheckoutModelExtension : ICheckoutModelExtension
{
public const string CheckoutBodyComponentName = "BitcoinCheckoutBody";
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly BTCPayNetwork _Network;
private readonly DisplayFormatter _displayFormatter;
private readonly IPaymentLinkExtension paymentLinkExtension;
private readonly IPaymentLinkExtension? lnPaymentLinkExtension;
private readonly IPaymentLinkExtension? lnurlPaymentLinkExtension;
private readonly string? _bech32Prefix;
private readonly PaymentMethodId lnPmi;
private readonly PaymentMethodId lnurlPmi;
public BitcoinCheckoutModelExtension(
PaymentMethodId paymentMethodId,
BTCPayNetwork network,
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
DisplayFormatter displayFormatter,
PaymentMethodHandlerDictionary handlers)
DisplayFormatter displayFormatter)
{
PaymentMethodId = paymentMethodId;
_handlers = handlers;
_Network = network;
_displayFormatter = displayFormatter;
lnPmi = PaymentTypes.LN.GetPaymentMethodId(_Network.CryptoCode);
lnurlPmi = PaymentTypes.LNURL.GetPaymentMethodId(_Network.CryptoCode);
paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
var lnPmi = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
lnPaymentLinkExtension = paymentLinkExtensions.SingleOrDefault(p => p.PaymentMethodId == lnPmi);
var lnurlPmi = PaymentTypes.LNURL.GetPaymentMethodId(network.CryptoCode);
lnurlPaymentLinkExtension = paymentLinkExtensions.SingleOrDefault(p => p.PaymentMethodId == lnurlPmi);
_bech32Prefix = network.NBitcoinNetwork.GetBech32Encoder(Bech32Type.WITNESS_PUBKEY_ADDRESS, false) is { } enc ? Encoders.ASCII.EncodeData(enc.HumanReadablePart) : null;
}
@ -47,11 +45,10 @@ namespace BTCPayServer.Payments.Bitcoin
public PaymentMethodId PaymentMethodId { get; }
public void ModifyCheckoutModel(CheckoutModelContext context)
{
if (context is not { Handler: BitcoinLikePaymentHandler handler })
return;
var handler = context.Handler;
var prompt = context.Prompt;
var details = handler.ParsePaymentPromptDetails(prompt.Details);
if (handler.ParsePaymentPromptDetails(prompt.Details) is not BitcoinPaymentPromptDetails details)
return;
context.Model.CheckoutBodyComponentName = CheckoutBodyComponentName;
context.Model.ShowRecommendedFee = context.StoreBlob.ShowRecommendedFee;
context.Model.FeeRate = details.RecommendedFeeRate.SatoshiPerByte;
@ -62,7 +59,6 @@ namespace BTCPayServer.Payments.Bitcoin
string? lightningFallback = null;
if (bip21Case)
{
var lnPmi = PaymentTypes.LN.GetPaymentMethodId(handler.Network.CryptoCode);
var lnPrompt = context.InvoiceEntity.GetPaymentPrompt(lnPmi);
if (lnPrompt is { Destination: not null })
{
@ -72,9 +68,10 @@ namespace BTCPayServer.Payments.Bitcoin
}
else
{
var lnurlPmi = PaymentTypes.LNURL.GetPaymentMethodId(handler.Network.CryptoCode);
var lnurlPrompt = context.InvoiceEntity.GetPaymentPrompt(lnurlPmi);
var lnUrl = lnurlPrompt is null ? null : lnurlPaymentLinkExtension?.GetPaymentLink(lnurlPrompt, context.UrlHelper);
var lnUrl = lnurlPrompt is null
? null
: lnurlPaymentLinkExtension?.GetPaymentLink(lnurlPrompt, context.UrlHelper);
if (lnUrl is not null)
lightningFallback = lnUrl;
@ -86,12 +83,15 @@ namespace BTCPayServer.Payments.Bitcoin
}
}
var paymentData = context.InvoiceEntity.GetAllBitcoinPaymentData(handler, true)?.MinBy(o => o.ConfirmationCount);
if (paymentData is not null)
if (handler is BitcoinLikePaymentHandler bitcoinHandler)
{
context.Model.RequiredConfirmations = NBXplorerListener.ConfirmationRequired(context.InvoiceEntity, paymentData);
context.Model.ReceivedConfirmations = paymentData.ConfirmationCount;
var paymentData = context.InvoiceEntity.GetAllBitcoinPaymentData(bitcoinHandler, true)?.MinBy(o => o.ConfirmationCount);
if (paymentData is not null)
{
context.Model.RequiredConfirmations = NBXplorerListener.ConfirmationRequired(context.InvoiceEntity, paymentData);
context.Model.ReceivedConfirmations = paymentData.ConfirmationCount;
}
}
// We're leading the way in Bitcoin community with adding UPPERCASE Bech32 addresses in QR Code

View File

@ -13,6 +13,7 @@ using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using PayoutData = BTCPayServer.Data.PayoutData;
using StoreData = BTCPayServer.Data.StoreData;
@ -21,7 +22,6 @@ namespace BTCPayServer.Payments.Lightning;
public class LightningPendingPayoutListener : BaseAsyncService
{
private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly ApplicationDbContextFactory _applicationDbContextFactory;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly StoreRepository _storeRepository;
private readonly IOptions<LightningNetworkOptions> _options;
@ -32,7 +32,6 @@ public class LightningPendingPayoutListener : BaseAsyncService
public LightningPendingPayoutListener(
LightningClientFactoryService lightningClientFactoryService,
ApplicationDbContextFactory applicationDbContextFactory,
PullPaymentHostedService pullPaymentHostedService,
StoreRepository storeRepository,
IOptions<LightningNetworkOptions> options,
@ -42,7 +41,6 @@ public class LightningPendingPayoutListener : BaseAsyncService
ILogger<LightningPendingPayoutListener> logger) : base(logger)
{
_lightningClientFactoryService = lightningClientFactoryService;
_applicationDbContextFactory = applicationDbContextFactory;
_pullPaymentHostedService = pullPaymentHostedService;
_storeRepository = storeRepository;
_options = options;
@ -54,19 +52,18 @@ public class LightningPendingPayoutListener : BaseAsyncService
private async Task Act()
{
await using var context = _applicationDbContextFactory.CreateContext();
var networks = _networkProvider.GetAll()
.OfType<BTCPayNetwork>()
.Where(network => network.SupportLightning)
.ToDictionary(network => PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode));
var payouts = await PullPaymentHostedService.GetPayouts(
var payouts = await _pullPaymentHostedService.GetPayouts(
new PullPaymentHostedService.PayoutQuery()
{
States = new PayoutState[] { PayoutState.InProgress },
PayoutMethods = networks.Keys.Select(id => id.ToString()).ToArray()
}, context);
});
var storeIds = payouts.Select(data => data.StoreDataId).Distinct();
var stores = (await Task.WhenAll(storeIds.Select(_storeRepository.FindStore)))
.Where(data => data is not null).ToDictionary(data => data.Id, data => (StoreData)data);
@ -83,9 +80,7 @@ public class LightningPendingPayoutListener : BaseAsyncService
.Select(c => (LightningPaymentMethodConfig)c.Value)
.FirstOrDefault();
if (pm is null)
{
continue;
}
var client =
pm.CreateLightningClient(networks[pmi], _options.Value, _lightningClientFactoryService);
@ -94,38 +89,54 @@ public class LightningPendingPayoutListener : BaseAsyncService
var handler = _payoutHandlers.TryGet(payoutData.GetPayoutMethodId()) as LightningLikePayoutHandler;
if (handler is null || handler.PayoutsPaymentProcessing.Contains(payoutData.Id))
continue;
using var track = handler.PayoutsPaymentProcessing.StartTracking();
if (!track.TryTrack(payoutData.Id))
continue;
var proof = handler.ParseProof(payoutData) as PayoutLightningBlob;
LightningPayment payment = null;
try
{
if (proof is not null)
payment = await client.GetPayment(proof.Id, CancellationToken);
payment = await client.GetPayment(proof.PaymentHash, CancellationToken);
}
catch
catch (OperationCanceledException)
{
// Do not mark as cancelled if the operation was cancelled.
// This can happen with Nostr GetPayment if the connection to relay is too slow.
continue;
}
if (payment is null)
{
payoutData.State = PayoutState.Cancelled;
continue;
}
switch (payment.Status)
{
case LightningPaymentStatus.Complete:
payoutData.State = PayoutState.Completed;
proof.Preimage = payment.Preimage;
payoutData.SetProofBlob(proof, null);
break;
case LightningPaymentStatus.Failed:
payoutData.State = PayoutState.Cancelled;
break;
}
payoutData.State = payment?.Status switch
{
LightningPaymentStatus.Complete => PayoutState.Completed,
LightningPaymentStatus.Failed => PayoutState.Cancelled,
LightningPaymentStatus.Unknown or LightningPaymentStatus.Pending => PayoutState.InProgress,
_ => PayoutState.Cancelled
};
if (payment is { Status: LightningPaymentStatus.Complete })
{
proof.Preimage = payment.Preimage;
payoutData.SetProofBlob(proof, null);
}
}
foreach (PayoutData payoutData in payoutByStoreByPaymentMethod)
{
if (payoutData.State != PayoutState.InProgress)
{
// This update can fail if the payout has been updated in the meantime
await _pullPaymentHostedService.MarkPaid(new HostedServices.MarkPayoutRequest()
{
PayoutId = payoutData.Id,
State = payoutData.State,
Proof = payoutData.State is PayoutState.Completed ? JObject.Parse(payoutData.Proof) : null
});
}
}
}
}
await context.SaveChangesAsync(CancellationToken);
await Task.Delay(TimeSpan.FromSeconds(SecondsDelay), CancellationToken);
}

View File

@ -104,7 +104,6 @@ namespace BTCPayServer.Payments
"XMR",
"ZEC",
"LCAD",
"ETB",
"LBTC",
"USDt",
"MONA",

View File

@ -110,7 +110,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
result = new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Destination = blob.Destination,
Message = claim.error
};
@ -118,7 +118,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
}
bool updateBlob = false;
if (result.Result is PayResult.Error or PayResult.CouldNotFindRoute && payoutData.State == PayoutState.AwaitingPayment)
if (result.Success is false && payoutData.State == PayoutState.AwaitingPayment)
{
updateBlob = true;
if (blob.IncrementErrorCount() >= 10)
@ -141,7 +141,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
new ResultVM
{
PayoutId = payoutId,
Result = PayResult.Error,
Success = false,
Message = "The payout isn't in a valid state"
};
@ -163,7 +163,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
return (null, new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Destination = blob.Destination,
Message =
$"The LNURL provided would not generate an invoice of {lm.ToDecimal(LightMoneyUnit.Satoshi)} sats"
@ -183,7 +183,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Destination = blob.Destination,
Message = e.Message
});
@ -204,7 +204,7 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Message = $"The BOLT11 invoice amount ({boltAmount} {payoutData.Currency}) did not match the payout's amount ({payoutData.Amount.GetValueOrDefault()} {payoutData.Currency})",
Destination = payoutBlob.Destination
};
@ -216,43 +216,52 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Success = false,
Message = $"The BOLT11 invoice expiry date ({bolt11PaymentRequest.ExpiryDate}) has expired",
Destination = payoutBlob.Destination
};
}
var proofBlob = new PayoutLightningBlob { PaymentHash = bolt11PaymentRequest.PaymentHash.ToString() };
PayResponse pay = null;
string errorReason = null;
string preimage = null;
bool? success = null;
LightMoney amountSent = null;
try
{
Exception exception = null;
try
{
pay = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
new PayInvoiceParams()
{
Amount = new LightMoney((decimal)payoutData.Amount, LightMoneyUnit.BTC)
}, cancellationToken);
if (pay?.Result is PayResult.CouldNotFindRoute)
var pay = await lightningClient.Pay(bolt11PaymentRequest.ToString(),
new PayInvoiceParams()
{
// Payment failed for sure... we can try again later!
payoutData.State = PayoutState.AwaitingPayment;
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.CouldNotFindRoute,
Message = $"Unable to find a route for the payment, check your channel liquidity",
Destination = payoutBlob.Destination
};
}
}
catch (Exception ex)
Amount = new LightMoney((decimal)payoutData.Amount, LightMoneyUnit.BTC)
}, cancellationToken);
if (pay is { Result: PayResult.CouldNotFindRoute })
{
exception = ex;
errorReason ??= $"Unable to find a route for the payment, check your channel liquidity";
success = false;
}
else if (pay is { Result: PayResult.Error })
{
errorReason ??= pay.ErrorDetail;
success = false;
}
else if (pay is { Result: PayResult.Ok })
{
if (pay.Details is { } details)
{
preimage = details.Preimage?.ToString();
amountSent = details.TotalAmount;
}
success = true;
}
}
catch (Exception ex)
{
errorReason ??= ex.Message;
}
if (success is null || preimage is null || amountSent is null)
{
LightningPayment payment = null;
try
{
@ -260,81 +269,49 @@ public class LightningAutomatedPayoutProcessor : BaseAutomatedPayoutProcessor<Li
}
catch (Exception ex)
{
exception = ex;
errorReason ??= ex.Message;
}
if (payment is null)
success ??= payment?.Status switch
{
payoutData.State = PayoutState.Cancelled;
var exceptionMessage = "";
if (exception is not null)
exceptionMessage = $" ({exception.Message})";
if (exceptionMessage == "")
exceptionMessage = $" ({pay?.ErrorDetail})";
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Message = $"Unable to confirm the payment of the invoice" + exceptionMessage,
Destination = payoutBlob.Destination
};
}
if (payment.Preimage is not null)
proofBlob.Preimage = payment.Preimage;
if (payment.Status == LightningPaymentStatus.Complete)
{
payoutData.State = PayoutState.Completed;
payoutData.SetProofBlob(proofBlob, null);
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Ok,
Destination = payoutBlob.Destination,
Message = payment.AmountSent != null
? $"Paid out {payment.AmountSent.ToDecimal(LightMoneyUnit.BTC)} {payoutData.Currency}"
: "Paid out"
};
}
else if (payment.Status == LightningPaymentStatus.Failed)
{
payoutData.State = PayoutState.AwaitingPayment;
string reason = "";
if (pay?.ErrorDetail is string err)
reason = $" ({err})";
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Error,
Destination = payoutBlob.Destination,
Message = $"The payment failed{reason}"
};
}
else
{
payoutData.State = PayoutState.InProgress;
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Unknown,
Destination = payoutBlob.Destination,
Message = "The payment has been initiated but is still in-flight."
};
}
LightningPaymentStatus.Complete => true,
LightningPaymentStatus.Failed => false,
_ => null
};
amountSent ??= payment?.AmountSent;
preimage ??= payment?.Preimage;
}
catch (OperationCanceledException)
if (preimage is not null)
proofBlob.Preimage = preimage;
var vm = new ResultVM
{
PayoutId = payoutData.Id,
Success = success,
Destination = payoutBlob.Destination
};
if (success is true)
{
payoutData.State = PayoutState.Completed;
payoutData.SetProofBlob(proofBlob, null);
vm.Message = amountSent != null
? $"Paid out {amountSent.ToDecimal(LightMoneyUnit.BTC)} {payoutData.Currency}"
: "Paid out";
}
else if (success is false)
{
payoutData.State = PayoutState.AwaitingPayment;
var err = errorReason is null ? "" : $" ({errorReason})";
vm.Message = $"The payment failed{err}";
}
else
{
// Timeout, potentially caused by hold invoices
// Payment will be saved as pending, the LightningPendingPayoutListener will handle settling/cancelling
payoutData.State = PayoutState.InProgress;
payoutData.SetProofBlob(proofBlob, null);
return new ResultVM
{
PayoutId = payoutData.Id,
Result = PayResult.Ok,
Destination = payoutBlob.Destination,
Message = "The payment timed out. We will verify if it completed later."
};
vm.Message = "The payment has been initiated but is still in-flight.";
}
return vm;
}
protected override async Task<bool> ProcessShouldSave(object paymentMethodConfig, List<PayoutData> payouts)

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.PayoutProcessors.Lightning;
@ -19,19 +20,22 @@ public class LightningAutomatedPayoutSenderFactory : IPayoutProcessorFactory
private readonly IServiceProvider _serviceProvider;
private readonly LinkGenerator _linkGenerator;
private readonly PayoutMethodId[] _supportedPayoutMethods;
private IStringLocalizer StringLocalizer { get; }
public LightningAutomatedPayoutSenderFactory(
PayoutMethodHandlerDictionary handlers,
IServiceProvider serviceProvider,
IStringLocalizer stringLocalizer,
LinkGenerator linkGenerator)
{
_handlers = handlers;
_serviceProvider = serviceProvider;
_linkGenerator = linkGenerator;
_supportedPayoutMethods = _handlers.OfType<LightningLikePayoutHandler>().Select(n => n.PayoutMethodId).ToArray();
StringLocalizer = stringLocalizer;
}
public string FriendlyName { get; } = "Automated Lightning Sender";
public string FriendlyName => StringLocalizer["Automated Lightning Sender"];
public string ConfigureLink(string storeId, PayoutMethodId payoutMethodId, HttpRequest request)
{

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
namespace BTCPayServer.PayoutProcessors.OnChain;
@ -20,18 +21,21 @@ public class OnChainAutomatedPayoutSenderFactory : EventHostedServiceBase, IPayo
private readonly IServiceProvider _serviceProvider;
private readonly LinkGenerator _linkGenerator;
private readonly PayoutMethodId[] _supportedPayoutMethods;
private IStringLocalizer StringLocalizer { get; }
public string FriendlyName { get; } = "Automated Bitcoin Sender";
public string FriendlyName => StringLocalizer["Automated Bitcoin Sender"];
public OnChainAutomatedPayoutSenderFactory(
PayoutMethodHandlerDictionary handlers,
EventAggregator eventAggregator,
ILogger<OnChainAutomatedPayoutSenderFactory> logger,
IStringLocalizer stringLocalizer,
IServiceProvider serviceProvider, LinkGenerator linkGenerator) : base(eventAggregator, logger)
{
_handlers = handlers;
_serviceProvider = serviceProvider;
_linkGenerator = linkGenerator;
_supportedPayoutMethods = _handlers.OfType<BitcoinLikePayoutHandler>().Select(c => c.PayoutMethodId).ToArray();
StringLocalizer = stringLocalizer;
}
public string Processor => ProcessorName;

View File

@ -1,15 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Hosting;
using BTCPayServer.Logging;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NBitcoin;
using NBitcoin.Protocol;
using NBXplorer;
namespace BTCPayServer.Plugins.Altcoins
@ -37,15 +28,12 @@ namespace BTCPayServer.Plugins.Altcoins
{
// Activating LBTC automatically activate the other liquid assets
InitUSDT(services, selectedChains, liquidNBX);
InitETB(services, selectedChains, liquidNBX);
InitLCAD(services, selectedChains, liquidNBX);
}
else
{
if (selectedChains.Contains("USDT"))
InitUSDT(services, selectedChains, liquidNBX);
if (selectedChains.Contains("ETB"))
InitETB(services, selectedChains, liquidNBX);
if (selectedChains.Contains("LCAD"))
InitLCAD(services, selectedChains, liquidNBX);
}

View File

@ -27,7 +27,11 @@ public partial class AltcoinsPlugin
CryptoImagePath = "imlegacy/liquid.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainName),
CoinType = ChainName == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true
SupportRBF = true,
SupportLightning = false,
SupportPayJoin = false,
VaultSupported = false,
ReadonlyWallet = true
}.SetDefaultElectrumMapping(ChainName);
var blockExplorerLink = ChainName == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}";

View File

@ -30,10 +30,13 @@ public partial class AltcoinsPlugin
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainName),
CoinType = ChainName == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
SupportLightning = false
SupportLightning = false,
SupportPayJoin = false,
VaultSupported = false,
ReadonlyWallet = true
}.SetDefaultElectrumMapping(ChainName);
services.AddBTCPayNetwork(network)
.AddTransactionLinkProvider(PaymentTypes.CHAIN.GetPaymentMethodId(nbxplorerNetwork.CryptoCode), new DefaultTransactionLinkProvider(LiquidBlockExplorer));
.AddTransactionLinkProvider(PaymentTypes.CHAIN.GetPaymentMethodId("USDt"), new DefaultTransactionLinkProvider(LiquidBlockExplorer));
services.AddCurrencyData(new CurrencyData()
{
Code = "USDt",
@ -45,35 +48,6 @@ public partial class AltcoinsPlugin
selectedChains.Add("LBTC");
}
private void InitETB(IServiceCollection services, SelectedChains selectedChains, NBXplorer.NBXplorerNetwork nbxplorerNetwork)
{
var network = new ElementsBTCPayNetwork()
{
CryptoCode = "ETB",
NetworkCryptoCode = "LBTC",
ShowSyncSummary = false,
DefaultRateRules = new[]
{
"ETB_X = ETB_BTC * BTC_X",
"ETB_BTC = bitpay(ETB_BTC)"
},
Divisibility = 2,
AssetId = new uint256("aa775044c32a7df391902b3659f46dfe004ccb2644ce2ddc7dba31e889391caf"),
DisplayName = "Ethiopian Birr",
NBXplorerNetwork = nbxplorerNetwork,
CryptoImagePath = "imlegacy/etb.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainName),
CoinType = ChainName == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
SupportLightning = false
}.SetDefaultElectrumMapping(ChainName);
services.AddBTCPayNetwork(network)
.AddTransactionLinkProvider(PaymentTypes.CHAIN.GetPaymentMethodId(nbxplorerNetwork.CryptoCode), new DefaultTransactionLinkProvider(LiquidBlockExplorer));
selectedChains.Add("LBTC");
}
string LiquidBlockExplorer => ChainName == ChainName.Mainnet ? "https://liquid.network/tx/{0}" : "https://liquid.network/testnet/tx/{0}";
private void InitLCAD(IServiceCollection services, SelectedChains selectedChains, NBXplorer.NBXplorerNetwork nbxplorerNetwork)
{
@ -95,11 +69,14 @@ public partial class AltcoinsPlugin
CryptoImagePath = "imlegacy/lcad.png",
DefaultSettings = BTCPayDefaultSettings.GetDefaultSettings(ChainName),
CoinType = ChainName == ChainName.Mainnet ? new KeyPath("1776'") : new KeyPath("1'"),
SupportRBF = true,
SupportLightning = false
SupportRBF = true,
SupportLightning = false,
SupportPayJoin = false,
VaultSupported = false,
ReadonlyWallet = true
}.SetDefaultElectrumMapping(ChainName);
services.AddBTCPayNetwork(network)
.AddTransactionLinkProvider(PaymentTypes.CHAIN.GetPaymentMethodId(nbxplorerNetwork.CryptoCode), new DefaultTransactionLinkProvider(LiquidBlockExplorer));
.AddTransactionLinkProvider(PaymentTypes.CHAIN.GetPaymentMethodId("LCAD"), new DefaultTransactionLinkProvider(LiquidBlockExplorer));
selectedChains.Add("LBTC");
}

View File

@ -228,8 +228,7 @@ namespace BTCPayServer.Plugins.Crowdfund
ProgressPercentage = (currentPayments.TotalCurrency / settings.TargetAmount) * 100,
PendingProgressPercentage = (pendingPayments.TotalCurrency / settings.TargetAmount) * 100,
LastUpdated = DateTime.UtcNow,
PaymentStats = GetPaymentStats(currentPayments),
PendingPaymentStats = GetPaymentStats(pendingPayments),
PaymentStats = GetPaymentStats(currentPayments, pendingPayments),
LastResetDate = lastResetDate,
NextResetDate = nextResetDate,
CurrentPendingAmount = pendingPayments.TotalCurrency,
@ -244,17 +243,21 @@ namespace BTCPayServer.Plugins.Crowdfund
return vm;
}
private Dictionary<string, PaymentStat> GetPaymentStats(InvoiceStatistics stats)
private Dictionary<string, PaymentStat> GetPaymentStats(InvoiceStatistics stats, InvoiceStatistics pendingSats)
{
var r = new Dictionary<string, PaymentStat>();
var total = stats.Select(s => s.Value.CurrencyValue).Sum();
foreach (var kv in stats)
var allStats = stats.Concat(pendingSats);
var total = allStats
.Select(s => s.Value.CurrencyValue).Sum();
foreach (var kv in allStats
.GroupBy(k => k.Key, k => k.Value)
.Select(g => (g.Key, CurrencyValue: g.Sum(s => s.CurrencyValue))))
{
var pmi = PaymentMethodId.Parse(kv.Key);
r.TryAdd(kv.Key, new PaymentStat()
{
Label = _prettyNameProvider.PrettyName(pmi),
Percent = (kv.Value.CurrencyValue / total) * 100.0m,
Percent = (kv.CurrencyValue / total) * 100.0m,
// Note that the LNURL will have the same LN
IsLightning = pmi == PaymentTypes.LN.GetPaymentMethodId(kv.Key)
});

View File

@ -56,7 +56,6 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
public decimal? PendingProgressPercentage { get; set; }
public DateTime LastUpdated { get; set; }
public Dictionary<string, PaymentStat> PaymentStats { get; set; }
public Dictionary<string, PaymentStat> PendingPaymentStats { get; set; }
public DateTime? LastResetDate { get; set; }
public DateTime? NextResetDate { get; set; }
}

View File

@ -65,5 +65,17 @@ namespace BTCPayServer.Plugins
var result = await httpClient.GetStringAsync($"api/v1/plugins{queryString}");
return JsonConvert.DeserializeObject<PublishedVersion[]>(result, serializerSettings) ?? throw new InvalidOperationException();
}
public async Task<PublishedVersion> GetPlugin(string pluginSlug, string version)
{
try
{
var result = await httpClient.GetStringAsync($"api/v1/plugins/{pluginSlug}/versions/{version}");
return JsonConvert.DeserializeObject<PublishedVersion>(result, serializerSettings);
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return null;
}
}
}
}

View File

@ -72,9 +72,11 @@ namespace BTCPayServer.Plugins
var dest = _dataDirectories.Value.PluginDir;
var filedest = Path.Join(dest, pluginIdentifier + ".btcpay");
var filemanifestdest = Path.Join(dest, pluginIdentifier + ".json");
var pluginSelector = $"[{Uri.EscapeDataString(pluginIdentifier)}]";
version = Uri.EscapeDataString(version);
Directory.CreateDirectory(Path.GetDirectoryName(filedest));
var url = $"api/v1/plugins/[{Uri.EscapeDataString(pluginIdentifier)}]/versions/{Uri.EscapeDataString(version)}/download";
var manifest = (await _pluginBuilderClient.GetPublishedVersions(null, true)).Select(v => v.ManifestInfo.ToObject<AvailablePlugin>()).FirstOrDefault(p => p.Identifier == pluginIdentifier);
var url = $"api/v1/plugins/{pluginSelector}/versions/{version}/download";
var manifest = (await _pluginBuilderClient.GetPlugin(pluginSelector, version))?.ManifestInfo?.ToObject<AvailablePlugin>();
await File.WriteAllTextAsync(filemanifestdest, JsonConvert.SerializeObject(manifest, Formatting.Indented));
using var resp2 = await _pluginBuilderClient.HttpClient.GetAsync(url);
await using var fs = new FileStream(filedest, FileMode.Create, FileAccess.ReadWrite);

View File

@ -22,7 +22,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
[Display(Name = "Display Title")]
public string Title { get; set; }
[MaxLength(5)]
[Display(Name = "Currency")]
public string Currency { get; set; }
[Display(Name = "Template")]
public string Template { get; set; }
[Display(Name = "Point of Sale Style")]
@ -77,23 +79,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
public string SearchTerm { get; set; }
public SelectList RedirectAutomaticallySelectList =>
new SelectList(new List<SelectListItem>()
new(new List<SelectListItem>
{
new SelectListItem()
{
Text = "Yes",
Value = "true"
},
new SelectListItem()
{
Text = "No",
Value = "false"
},
new SelectListItem()
{
Text = "Use Store Settings",
Value = ""
}
new() { Text = "Yes", Value = "true" },
new() { Text = "No", Value = "false" },
new() { Text = "Use Store Settings", Value = "" }
}, nameof(SelectListItem.Value), nameof(SelectListItem.Text), RedirectAutomatically);
public string Description { get; set; }

View File

@ -91,7 +91,7 @@ namespace BTCPayServer
}
catch (Exception e) when (PluginManager.IsExceptionByPlugin(e, out var pluginName))
{
logs.Configuration.LogError(e, $"Disabling plugin {pluginName} as it crashed on startup");
logs.Configuration.LogError(e, $"Plugin crash during startup detected, disabling {pluginName}...");
var pluginDir = new DataDirectories().Configure(conf).PluginDir;
PluginManager.DisablePlugin(pluginDir, pluginName);
}

View File

@ -3,6 +3,7 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Services.Notifications.Blobs
{
@ -14,11 +15,13 @@ namespace BTCPayServer.Services.Notifications.Blobs
{
private readonly LinkGenerator _linkGenerator;
private readonly BTCPayServerOptions _options;
private IStringLocalizer StringLocalizer { get; }
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options)
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options, IStringLocalizer stringLocalizer)
{
_linkGenerator = linkGenerator;
_options = options;
StringLocalizer = stringLocalizer;
}
public override string NotificationType => TYPE;
@ -27,7 +30,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
{
get
{
return new (string identifier, string name)[] { (TYPE, "External payout approval") };
return new (string identifier, string name)[] { (TYPE, StringLocalizer["External payout approval"]) };
}
}
@ -38,7 +41,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
vm.Type = notification.NotificationType;
vm.StoreId = notification.StoreId;
vm.Body =
"A payment that was made to an approved payout by an external wallet is waiting for your confirmation.";
StringLocalizer["A payment that was made to an approved payout by an external wallet is waiting for your confirmation."];
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts),
"UIStorePullPayments",
new

View File

@ -3,6 +3,7 @@ using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Services.Notifications.Blobs;
@ -28,15 +29,16 @@ internal class InviteAcceptedNotification : BaseNotification
StoreName = store.StoreName;
}
internal class Handler(LinkGenerator linkGenerator, BTCPayServerOptions options)
internal class Handler(LinkGenerator linkGenerator, BTCPayServerOptions options, IStringLocalizer stringLocalizer)
: NotificationHandler<InviteAcceptedNotification>
{
private IStringLocalizer StringLocalizer { get; } = stringLocalizer;
public override string NotificationType => TYPE;
public override (string identifier, string name)[] Meta
{
get
{
return [(TYPE, "User accepted invitation")];
return [(TYPE, StringLocalizer["User accepted invitation"])];
}
}
@ -45,7 +47,7 @@ internal class InviteAcceptedNotification : BaseNotification
vm.Identifier = notification.Identifier;
vm.Type = notification.NotificationType;
vm.StoreId = notification.StoreId;
vm.Body = $"User {notification.UserEmail} accepted the invite to {notification.StoreName}.";
vm.Body = StringLocalizer["User {0} accepted the invite to {1}.", notification.UserEmail, notification.StoreName];
vm.ActionLink = linkGenerator.GetPathByAction(nameof(UIStoresController.StoreUsers),
"UIStores",
new { storeId = notification.StoreId }, options.RootPath);

View File

@ -6,6 +6,7 @@ using BTCPayServer.Controllers;
using BTCPayServer.Events;
using ExchangeSharp;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Services.Notifications.Blobs
{
@ -16,11 +17,13 @@ namespace BTCPayServer.Services.Notifications.Blobs
{
private readonly LinkGenerator _linkGenerator;
private readonly BTCPayServerOptions _options;
private IStringLocalizer StringLocalizer { get; }
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options)
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options, IStringLocalizer stringLocalizer)
{
_linkGenerator = linkGenerator;
_options = options;
StringLocalizer = stringLocalizer;
}
public override string NotificationType => TYPE;
@ -29,25 +32,25 @@ namespace BTCPayServer.Services.Notifications.Blobs
{
get
{
return new (string identifier, string name)[] { (TYPE, "All invoice updates"), }
.Concat(TextMapping.Select(pair => ($"{TYPE}_{pair.Key}", $"Invoice {pair.Value}"))).ToArray();
return new (string identifier, string name)[] { (TYPE, StringLocalizer["All invoice updates"]), }
.Concat(TextMapping.Select(pair => ($"{TYPE}_{pair.Key}", StringLocalizer["Invoice {0}", pair.Value].Value))).ToArray();
}
}
internal static Dictionary<string, string> TextMapping = new Dictionary<string, string>()
private Dictionary<string, string> TextMapping => new()
{
// {InvoiceEvent.PaidInFull, "was fully paid"},
{InvoiceEvent.PaidAfterExpiration, "was paid after expiration"},
{InvoiceEvent.ExpiredPaidPartial, "expired with partial payments"},
{InvoiceEvent.FailedToConfirm, "has payments that failed to confirm on time"},
// {InvoiceEvent.ReceivedPayment, "received a payment"},
{InvoiceEvent.Confirmed, "is settled"}
// {InvoiceEvent.PaidInFull, StringLocalizer["was fully paid"},
{InvoiceEvent.PaidAfterExpiration, StringLocalizer["was paid after expiration"]},
{InvoiceEvent.ExpiredPaidPartial, StringLocalizer["expired with partial payments"]},
{InvoiceEvent.FailedToConfirm, StringLocalizer["has payments that failed to confirm on time"]},
// {InvoiceEvent.ReceivedPayment, StringLocalizer["received a payment"},
{InvoiceEvent.Confirmed, StringLocalizer["is settled"]}
};
protected override void FillViewModel(InvoiceEventNotification notification,
NotificationViewModel vm)
{
var baseStr = $"Invoice {notification.InvoiceId.Substring(0, 5)}..";
var baseStr = StringLocalizer["Invoice {0}..", notification.InvoiceId.Substring(0, 5)];
if (TextMapping.ContainsKey(notification.Event))
{
vm.Body = $"{baseStr} {TextMapping[notification.Event]}";
@ -74,7 +77,12 @@ namespace BTCPayServer.Services.Notifications.Blobs
public static bool HandlesEvent(string invoiceEvent)
{
return Handler.TextMapping.Keys.Any(s => s == invoiceEvent);
return ((string[])[
InvoiceEvent.PaidAfterExpiration,
InvoiceEvent.ExpiredPaidPartial,
InvoiceEvent.FailedToConfirm,
InvoiceEvent.Confirmed])
.Any(s => s == invoiceEvent);
}
public string InvoiceId { get; set; }

View File

@ -3,6 +3,7 @@ using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using BTCPayServer.Data;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Services.Notifications.Blobs;
@ -28,11 +29,13 @@ internal class NewUserRequiresApprovalNotification : BaseNotification
{
private readonly LinkGenerator _linkGenerator;
private readonly BTCPayServerOptions _options;
private IStringLocalizer StringLocalizer { get; }
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options)
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options, IStringLocalizer stringLocalizer)
{
_linkGenerator = linkGenerator;
_options = options;
StringLocalizer = stringLocalizer;
}
public override string NotificationType => TYPE;
@ -40,7 +43,7 @@ internal class NewUserRequiresApprovalNotification : BaseNotification
{
get
{
return [(TYPE, "New user requires approval")];
return [(TYPE, StringLocalizer["New user requires approval"])];
}
}
@ -48,7 +51,7 @@ internal class NewUserRequiresApprovalNotification : BaseNotification
{
vm.Identifier = notification.Identifier;
vm.Type = notification.NotificationType;
vm.Body = $"New user {notification.UserEmail} requires approval.";
vm.Body = StringLocalizer["New user {0} requires approval.", notification.UserEmail];
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIServerController.User),
"UIServer",
new { userId = notification.UserId }, _options.RootPath);

View File

@ -1,19 +1,20 @@
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Models.NotificationViewModels;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Services.Notifications.Blobs
{
internal class NewVersionNotification : BaseNotification
{
private const string TYPE = "newversion";
internal class Handler : NotificationHandler<NewVersionNotification>
internal class Handler(IStringLocalizer stringLocalizer) : NotificationHandler<NewVersionNotification>
{
private IStringLocalizer StringLocalizer { get; } = stringLocalizer;
public override string NotificationType => TYPE;
public override (string identifier, string name)[] Meta
{
get
{
return new (string identifier, string name)[] { (TYPE, "New version") };
return [(TYPE, StringLocalizer["New version"])];
}
}
@ -21,7 +22,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
{
vm.Identifier = notification.Identifier;
vm.Type = notification.NotificationType;
vm.Body = $"New version {notification.Version} released!";
vm.Body = StringLocalizer["New version {0} released!", notification.Version];
vm.ActionLink = $"https://github.com/btcpayserver/btcpayserver/releases/tag/v{notification.Version}";
}
}

View File

@ -5,6 +5,7 @@ using BTCPayServer.Configuration;
using BTCPayServer.Controllers;
using ExchangeSharp;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Services.Notifications.Blobs
{
@ -16,11 +17,13 @@ namespace BTCPayServer.Services.Notifications.Blobs
{
private readonly LinkGenerator _linkGenerator;
private readonly BTCPayServerOptions _options;
private IStringLocalizer StringLocalizer { get; }
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options)
public Handler(LinkGenerator linkGenerator, BTCPayServerOptions options, IStringLocalizer stringLocalizer)
{
_linkGenerator = linkGenerator;
_options = options;
StringLocalizer = stringLocalizer;
}
public override string NotificationType => TYPE;
@ -28,7 +31,7 @@ namespace BTCPayServer.Services.Notifications.Blobs
{
get
{
return new (string identifier, string name)[] { (TYPE, "Payouts") };
return [(TYPE, StringLocalizer["Payouts"])];
}
}
@ -39,8 +42,8 @@ namespace BTCPayServer.Services.Notifications.Blobs
vm.StoreId = notification.StoreId;
vm.Body = (notification.Status ?? PayoutState.AwaitingApproval) switch
{
PayoutState.AwaitingApproval => "A new payout is awaiting for approval",
PayoutState.AwaitingPayment => "A new payout is approved and awaiting payment",
PayoutState.AwaitingApproval => StringLocalizer["A new payout is awaiting for approval"],
PayoutState.AwaitingPayment => StringLocalizer["A new payout is approved and awaiting payment"],
_ => throw new ArgumentOutOfRangeException()
};
vm.ActionLink = _linkGenerator.GetPathByAction(nameof(UIStorePullPaymentsController.Payouts),

File diff suppressed because it is too large Load Diff

View File

@ -67,7 +67,7 @@
{
<th class="text-end">
<span text-translate="true">Network Fee</span>
<a href="https://docs.btcpayserver.org/FAQ/Stores/#allow-anyone-to-create-invoice" target="_blank" rel="noreferrer noopener" title="More information...">
<a href="https://docs.btcpayserver.org/FAQ/Stores/#allow-anyone-to-create-invoice" target="_blank" rel="noreferrer noopener" title="@StringLocalizer["More information..."]">
<vc:icon symbol="info" />
</a>
</th>

View File

@ -11,7 +11,7 @@
<h5 class="modal-title">
{{title}}
</h5>
<button type="button" class="btn-close" aria-label="Close" v-on:click="close">
<button type="button" class="btn-close" aria-label="@StringLocalizer["Close"]" v-on:click="close">
<vc:icon symbol="close"/>
</button>
</div>
@ -255,15 +255,15 @@ function initCameraScanningApp(title, onDataSubmit, modalId, submitOnScan = fals
if (error.name === 'StreamApiNotSupportedError') {
this.noStreamApiSupport = true;
} else if (error.name === 'NotAllowedError') {
this.errorMessage = 'A permission to the camera is needed to scan the QR code. Please grant the browser access and then retry.'
this.errorMessage = @StringLocalizer["A permission to the camera is needed to scan the QR code. Please grant the browser access and then retry."]
} else if (error.name === 'NotFoundError') {
this.errorMessage = 'A camera was not detected on your device.'
this.errorMessage = @StringLocalizer["A camera was not detected on your device."]
} else if (error.name === 'NotSupportedError') {
this.errorMessage = 'This page is served in non-secure context (HTTPS, localhost or file://)'
this.errorMessage = @StringLocalizer["This page is served in non-secure context (HTTPS, localhost or file://)"]
} else if (error.name === 'NotReadableError') {
this.errorMessage = 'Couldn\'t access your camera. Is it already in use?'
this.errorMessage = @StringLocalizer["Could not access your camera. Is it already in use?"]
} else if (error.name === 'OverconstrainedError') {
this.errorMessage = 'Constraints don\'t match any installed camera.'
this.errorMessage = @StringLocalizer["Constraints do not match any installed camera."]
} else {
this.errorMessage = 'UNKNOWN ERROR: ' + error.message
}

View File

@ -4,15 +4,15 @@
string actionUrl = null;
if (Model.ActionName is not null)
{
var controllerName = Model.ControllerName ?? ((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)this.Url.ActionContext.ActionDescriptor).ControllerName;
actionUrl = linkGenerator.GetPathByAction(Model.ActionName, controllerName, pathBase: this.Context.Request.PathBase);
var controllerName = Model.ControllerName ?? ((Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor)Url.ActionContext.ActionDescriptor).ControllerName;
actionUrl = linkGenerator.GetPathByAction(Model.ActionName, controllerName, pathBase: Context.Request.PathBase);
}
}
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="ConfirmTitle">@Model.Title</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
</div>

View File

@ -249,7 +249,7 @@
</div>
</div>
</noscript>
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="Close" ref="modalContribute">
<b-modal title="Contribute" v-model="contributeModalOpen" size="lg" ok-only="true" ok-variant="secondary" ok-title="@StringLocalizer["Close"]" ref="modalContribute">
<contribute v-if="contributeModalOpen"
:target-currency="srvModel.targetCurrency"
:active="active"
@ -330,7 +330,7 @@
:readonly="perk.priceType === 'Fixed'"
:min="perk.price"
step="any"
placeholder="Contribution Amount"
placeholder="@StringLocalizer["Contribution Amount"]"
required>
<span class="input-group-text">{{targetCurrency}}</span>
</template>

View File

@ -11,7 +11,7 @@
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
@model BTCPayServer.Plugins.Crowdfund.Models.UpdateCrowdfundViewModel
@{
ViewData.SetActivePage(AppsNavPages.Update, "Update Crowdfund", Model.AppId);
ViewData.SetActivePage(AppsNavPages.Update, StringLocalizer["Update Crowdfund"], Model.AppId);
Csp.UnsafeEval();
var canUpload = await FileService.IsAvailable();
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
@ -35,11 +35,11 @@
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
@if (Model.Archived)
{
<button type="submit" class="btn btn-outline-secondary" name="Archived" value="False">Unarchive</button>
<button type="submit" class="btn btn-outline-secondary" name="Archived" value="False" text-translate="true">Unarchive</button>
}
else if (Model.ModelWithMinimumData)
{
<a class="btn btn-secondary" asp-controller="UICrowdfund" asp-action="ViewCrowdfund" asp-route-appId="@Model.AppId" id="ViewApp" target="_blank">View</a>
<a class="btn btn-secondary" asp-controller="UICrowdfund" asp-action="ViewCrowdfund" asp-route-appId="@Model.AppId" id="ViewApp" target="_blank" text-translate="true">View</a>
}
</div>
</div>
@ -82,7 +82,7 @@
@if (!string.IsNullOrEmpty(Model.MainImageUrl))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveLogoFile" value="true">
<vc:icon symbol="cross" /> Remove
<vc:icon symbol="cross" /> <span text-translate="true">Remove</span>
</button>
}
</div>
@ -100,7 +100,7 @@
else
{
<input asp-for="MainImageFile" type="file" class="form-control" disabled>
<div class="form-text">In order to upload an image, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</div>
<div class="form-text">@ViewLocalizer["In order to upload, a {0} must be configured.", Html.ActionLink(StringLocalizer["file storage"], "Files", "UIServer")]</div>
}
</div>
<div class="form-group">
@ -136,7 +136,7 @@
<div class="form-group">
<label asp-for="TargetCurrency" class="form-label"></label>
<input asp-for="TargetCurrency" class="form-control w-auto" currency-selection />
<div class="form-text">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</div>
<div class="form-text">@StringLocalizer["Uses the store's default currency ({0}) if empty.", @Model.StoreDefaultCurrency]</div>
<span asp-validation-for="TargetCurrency" class="text-danger"></span>
</div>
</div>
@ -147,7 +147,7 @@
<input type="datetime-local" asp-for="StartDate"
value="@(Model.StartDate?.ToString("u", CultureInfo.InvariantCulture))"
class="form-control flatdtpicker"
placeholder="No start date has been set" />
placeholder="@StringLocalizer["No start date has been set"]" />
<button class="btn btn-secondary input-group-clear px-3" type="button" title="Clear">
<vc:icon symbol="close"/>
</button>
@ -159,7 +159,7 @@
<input type="datetime-local" asp-for="EndDate"
value="@(Model.EndDate?.ToString("u", CultureInfo.InvariantCulture))"
class="form-control flatdtpicker"
placeholder="No end date has been set" />
placeholder="@StringLocalizer["No end date has been set"]" />
<button class="btn btn-secondary input-group-clear px-3" type="button" title="Clear">
<vc:icon symbol="close"/>
</button>
@ -183,7 +183,7 @@
<div class="form-group mb-0 pt-2 w-250px">
<label asp-for="ResetEveryAmount" class="form-label"></label>
<div class="d-flex align-items-center">
<input type="number" inputmode="numeric" asp-for="ResetEveryAmount" placeholder="Amount" class="form-control me-3" min="0">
<input type="number" inputmode="numeric" asp-for="ResetEveryAmount" placeholder="@StringLocalizer["Amount"]" class="form-control me-3" min="0">
<select class="form-select w-auto" asp-for="ResetEvery">
@foreach (var opt in Model.ResetEveryValues)
{

View File

@ -36,7 +36,7 @@
</div>
<div class="form-group">
<label asp-for="Settings.From" class="form-label" text-translate="true">Sender's Email Address</label>
<input asp-for="Settings.From" class="form-control" placeholder="Firstname Lastname <email@example.com>" />
<input asp-for="Settings.From" class="form-control" placeholder="@StringLocalizer["Firstname Lastname <email@example.com>"]" />
<span asp-validation-for="Settings.From" class="text-danger"></span>
</div>
<div class="form-group">
@ -55,8 +55,8 @@
else
{
<div class="input-group">
<input value="Configured" type="text" readonly class="form-control"/>
<button type="submit" class="btn btn-danger" name="command" value="ResetPassword" id="ResetPassword">Reset</button>
<input value="@StringLocalizer["Configured"]" type="text" readonly class="form-control"/>
<button type="submit" class="btn btn-danger" name="command" value="ResetPassword" id="ResetPassword" text-translate="true">Reset</button>
</div>
}
</div>

View File

@ -5,7 +5,7 @@
<div class="col-xl-10 col-xxl-constrain">
<div class="form-group">
<label asp-for="TestEmail" class="form-label" text-translate="true">To test your settings, enter an email address</label>
<input asp-for="TestEmail" placeholder="Firstname Lastname <email@example.com>" class="form-control" />
<input asp-for="TestEmail" placeholder="@StringLocalizer["Firstname Lastname <email@example.com>"]" class="form-control" />
<span asp-validation-for="TestEmail" class="text-danger"></span>
</div>
<button type="submit" class="btn btn-secondary mt-2" name="command" value="Test" id="Test" text-translate="true">Send Test Email</button>

View File

@ -7,9 +7,9 @@
var storeId = Context.GetRouteValue("storeId") as string;
var controller = ViewContext.RouteData.Values["controller"].ToString().TrimEnd("Controller", StringComparison.InvariantCultureIgnoreCase);
if (string.IsNullOrEmpty(storeId))
ViewData.SetActivePage(ServerNavPages.Roles);
ViewData.SetActivePage(ServerNavPages.Roles, StringLocalizer["Roles"]);
else
ViewData.SetActivePage(StoreNavPages.Roles);
ViewData.SetActivePage(StoreNavPages.Roles, StringLocalizer["Roles"]);
var permission = string.IsNullOrEmpty(storeId) ? Policies.CanModifyServerSettings : Policies.CanModifyStoreSettings;
var nextRoleSortOrder = (string) ViewData["NextRoleSortOrder"];
var roleSortOrder = nextRoleSortOrder switch
@ -19,8 +19,8 @@
_ => null
};
const string sortByDesc = "Sort by name descending...";
const string sortByAsc = "Sort by name ascending...";
var sortByDesc = StringLocalizer["Sort by name descending..."];
var sortByAsc = StringLocalizer["Sort by name ascending..."];
var showInUseColumn = !Model.Roles.Any(r => r.IsUsed is null);
}

View File

@ -1,5 +1,5 @@
<div id="walletAlert" class="alert alert-warning alert-dismissible my-4" style="display:none;" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
<span id="alertMessage"></span>

View File

@ -1,6 +1,7 @@
<div class="alert alert-warning alert-dismissible">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
The Email settings have not been configured on this server or store yet. Setting this field will not send emails until then. <a asp-action="StoreEmailSettings" asp-controller="UIStores" asp-route-storeId="@Model" class="alert-link">Configure store email settings</a>
<span text-translate="true">The Email settings have not been configured on this server or store yet. Setting this field will not send emails until then.</span>
<a asp-action="StoreEmailSettings" asp-controller="UIStores" asp-route-storeId="@Model" class="alert-link" text-translate="true">Configure store email settings</a>
</div>

View File

@ -1,4 +1,7 @@
@using BTCPayServer.TagHelpers
@using BTCPayServer.Views.Stores
@using Microsoft.AspNetCore.Html
@using Microsoft.AspNetCore.Mvc.TagHelpers
@{
ViewData.SetActivePage(StoreNavPages.PayButton, "Pay Button", Context.GetStoreData().Id);
}
@ -12,25 +15,20 @@
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<div class="alert alert-warning alert-dismissible mb-4" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
<h5 class="alert-heading">Warning: Payment button should only be used for tips and donations</h5>
<h5 class="alert-heading" text-translate="true">Warning: Payment button should only be used for tips and donations</h5>
<p>
Using the payment button for e-commerce integrations is not recommended since order relevant information can be modified by the user.
For e-commerce, you should use our
<a href="https://docs.btcpayserver.org/API/Greenfield/v1/" class="alert-link" target="_blank" rel="noreferrer noopener">Greenfield API</a>.
If this store process commercial transactions, we advise you to
<a asp-controller="UIUserStores" asp-action="CreateStore" class="alert-link">create a separate store</a> before using the payment button.
@ViewLocalizer["Using the payment button for e-commerce integrations is not recommended since order relevant information can be modified by the user. For e-commerce, you should use our {0}. If this store process commercial transactions, we advise you to {1} before using the payment button.",
Html.ActionLink(StringLocalizer["Greenfield API"], "SwaggerDocs", "UIHome", new { }, new { @class = "alert-link" }),
Html.ActionLink(StringLocalizer["create a separate store"], "CreateStore", "UIUserStores", new { }, new { @class = "alert-link" })]
</p>
</div>
<p>
To start using Pay Button, you need to enable this feature explicitly.
Once you do so, anyone could create an invoice on your store (via API, for example).
</p>
<p text-translate="true">To start using Pay Button, you need to enable this feature explicitly. Once you do so, anyone could create an invoice on your store (via API, for example).</p>
<form method="post">
@Html.Hidden("EnableStore", true)
<button name="command" id="enable-pay-button" type="submit" value="save" class="btn btn-primary">
<button name="command" id="enable-pay-button" type="submit" value="save" class="btn btn-primary" text-translate="true">
Enable
</button>
</form>

View File

@ -1,4 +1,5 @@
@using BTCPayServer.Views.Stores
@using Microsoft.AspNetCore.Html
@inject Security.ContentSecurityPolicies Csp
@inject BTCPayNetworkProvider NetworkProvider
@model BTCPayServer.Plugins.PayButton.Models.PayButtonViewModel
@ -184,77 +185,75 @@
<div class="row">
<div class="col-xl-8">
<div class="alert alert-warning alert-dismissible mb-4" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
<h5 class="alert-heading">Warning: Payment button should only be used for tips and donations</h5>
<h5 class="alert-heading" text-translate="true">Warning: Payment button should only be used for tips and donations</h5>
<p>
Using the payment button for e-commerce integrations is not recommended since order relevant information can be modified by the user.
For e-commerce, you should use our
<a href="https://docs.btcpayserver.org/API/Greenfield/v1/" class="alert-link" target="_blank" rel="noreferrer noopener">Greenfield API</a>.
If this store process commercial transactions, we advise you to
<a asp-controller="UIUserStores" asp-action="CreateStore" class="alert-link">create a separate store</a> before using the payment button.
@ViewLocalizer["Using the payment button for e-commerce integrations is not recommended since order relevant information can be modified by the user. For e-commerce, you should use our {0}. If this store process commercial transactions, we advise you to {1} before using the payment button.",
Html.ActionLink(StringLocalizer["Greenfield API"], "SwaggerDocs", "UIHome", new { }, new { @class = "alert-link" }),
Html.ActionLink(StringLocalizer["create a separate store"], "CreateStore", "UIUserStores", new { }, new { @class = "alert-link" })]
</p>
<form asp-action="DisableAnyoneCanCreateInvoice" asp-route-storeId="@Context.GetRouteValue("storeId")" method="post">
<button name="command" id="disable-pay-button" type="submit" class="btn btn-danger mt-0" value="Save">Disable payment button</button>
<button name="command" id="disable-pay-button" type="submit" class="btn btn-danger mt-0" value="Save" text-translate="true">Disable payment button</button>
</form>
</div>
<p>Configure your Pay Button, and the generated code will be displayed at the bottom of the page to copy into your project.</p>
<h4 class="mt-3 mb-3">General Settings</h4>
<p text-translate="true">Configure your Pay Button, and the generated code will be displayed at the bottom of the page to copy into your project.</p>
<h4 class="mt-3 mb-3" text-translate="true">General Settings</h4>
<div class="form-group col-md-8">
<label class="form-label" for="price">Price</label>
<label class="form-label" for="price" text-translate="true">Price</label>
<input name="price" type="text" class="form-control" id="price" inputmode="decimal"
v-model="srvModel.price" v-on:change="inputChanges"
v-validate="'decimal|min_value:0'" :class="{'is-invalid': errors.has('price') }">
<small class="text-danger">{{ errors.first('price') }}</small>
</div>
<div class="form-group col-md-4" v-if="!srvModel.appIdEndpoint">
<label class="form-label" for="Currency">Currency</label>
<label class="form-label" for="Currency" text-translate="true">Currency</label>
<input asp-for="Currency" name="currency" class="form-control w-auto" currency-selection
v-model="srvModel.currency" v-on:change="inputChanges"
:class="{'is-invalid': errors.has('currency') }" />
</div>
<div class="form-group col-md-4" v-if="!srvModel.appIdEndpoint">
<label class="form-label" for="defaultPaymentMethod">Default Payment Method</label>
<label class="form-label" for="defaultPaymentMethod" text-translate="true">Default Payment Method</label>
<select v-model="srvModel.defaultPaymentMethod" v-on:change="inputChanges" class="form-select" id="default-payment-method">
<option value="" selected>Use the stores default</option>
<option value="" selected text-translate="true">Use the stores default</option>
<option v-for="pm in srvModel.paymentMethods" v-bind:value="pm.value">{{pm.name}}</option>
</select>
</div>
<div class="form-group" v-if="!srvModel.appIdEndpoint">
<label class="form-label" for="description">Checkout Description</label>
<label class="form-label" for="description" text-translate="true">Checkout Description</label>
<input name="checkoutDesc" type="text" class="form-control" id="description"
v-model="srvModel.checkoutDesc" v-on:change="inputChanges">
</div>
<div class="form-group">
<label class="form-label" for="order-id">Order ID</label>
<label class="form-label" for="order-id" text-translate="true">Order ID</label>
<input name="orderId" type="text" class="form-control" id="order-id"
v-model="srvModel.orderId" v-on:change="inputChanges">
</div>
</div>
</div>
<h4 class="mt-5 mb-3">Display Options</h4>
<h4 class="mt-5 mb-3" text-translate="true">Display Options</h4>
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<div class="form-group">
<div class="form-check" v-if="!srvModel.appIdEndpoint">
<input id="useModal" type="checkbox" v-model="srvModel.useModal" v-on:change="inputChanges" class="form-check-input"/>
<label for="useModal" class="form-check-label">Use Modal</label>
<label for="useModal" class="form-check-label" text-translate="true">Use Modal</label>
</div>
<div class="form-check">
<input id="buttonInlineTextMode" type="checkbox" v-model="buttonInlineTextMode" v-on:change="inputChanges" class="form-check-input"/>
<label for="buttonInlineTextMode" class="form-check-label">Customize Pay Button Text</label>
<label for="buttonInlineTextMode" class="form-check-label" text-translate="true">Customize Pay Button Text</label>
</div>
</div>
<div class="form-group" v-show="buttonInlineTextMode">
<label class="form-label" for="pb-text">Pay Button Text</label>
<label class="form-label" for="pb-text" text-translate="true">Pay Button Text</label>
<input name="payButtonText" type="text" class="form-control" id="pb-text"
v-model="srvModel.payButtonText" v-on:change="inputChanges">
</div>
<div class="form-group mb-4">
<label class="form-label" for="pb-image-url">Pay Button Image Url</label>
<label class="form-label" for="pb-image-url" text-translate="true">Pay Button Image Url</label>
<input name="payButtonImageUrl" type="text" class="form-control" id="pb-image-url"
v-model="srvModel.payButtonImageUrl" v-on:change="inputChanges"
v-validate="{ required: this.imageUrlRequired, url: {require_tld:false} }"
@ -262,7 +261,7 @@
<small class="text-danger">{{ errors.first('payButtonImageUrl') }}</small>
</div>
<div class="form-group mb-4">
<label class="form-label">Image Size</label>
<label class="form-label" text-translate="true">Image Size</label>
<div class="btn-group d-flex" role="group">
<button type="button" class="btn btn-outline-secondary"
v-on:click="inputChanges($event, 0)">146 x 40 px</button>
@ -273,37 +272,37 @@
</div>
</div>
<div class="form-group">
<label class="form-label">Button Type</label>
<label class="form-label" text-translate="true">Button Type</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="button-type" id="btn-fixed" value="0" v-model="srvModel.buttonType" v-on:change="inputChanges" checked/>
<label for="btn-fixed" class="form-check-label">Fixed amount</label>
<label for="btn-fixed" class="form-check-label" text-translate="true">Fixed amount</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="button-type" id="btn-custom" value="1" v-model="srvModel.buttonType" v-on:change="inputChanges"/>
<label for="btn-custom" class="form-check-label">Custom amount</label>
<label for="btn-custom" class="form-check-label" text-translate="true">Custom amount</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="button-type" id="btn-slider" value="2" v-model="srvModel.buttonType" v-on:change="inputChanges"/>
<label for="btn-slider" class="form-check-label">Slider</label>
<label for="btn-slider" class="form-check-label" text-translate="true">Slider</label>
</div>
</div>
<div class="row" v-if="srvModel.buttonType === '1' ||srvModel.buttonType === '2'">
<div class="form-group col-md-4">
<label class="form-label" for="pb-min">Min</label>
<label class="form-label" for="pb-min" text-translate="true">Min</label>
<input name="min" type="text" class="form-control" id="pb-min"
v-model="srvModel.min" v-on:change="inputChanges"
v-validate="'required|decimal|min_value:0'" :class="{'is-invalid': errors.has('min') }">
<small class="text-danger">{{ errors.first('min') }}</small>
</div>
<div class="form-group col-md-4">
<label class="form-label" for="pb-max">Max</label>
<label class="form-label" for="pb-max" text-translate="true">Max</label>
<input name="max" type="text" class="form-control" id="pb-max"
v-model="srvModel.max" v-on:change="inputChanges"
v-validate="'required|decimal'" :class="{'is-invalid': errors.has('max') }">
<small class="text-danger">{{ errors.first('max') }}</small>
</div>
<div class="form-group col-md-4">
<label class="form-label" for="pb-step">Step</label>
<label class="form-label" for="pb-step" text-translate="true">Step</label>
<input name="step" type="text" class="form-control" id="pb-step"
v-model="srvModel.step" v-on:change="inputChanges"
v-validate="'required'" :class="{'is-invalid': errors.has('step') }">
@ -319,7 +318,7 @@
v-model="srvModel.simpleInput"
v-on:change="inputChanges"
:class="{'is-invalid': errors.has('simpleInput') }">
<label class="form-check-label" for="simpleInput">Use a simple input style</label>
<label class="form-check-label" for="simpleInput" text-translate="true">Use a simple input style</label>
<small class="text-danger">{{ errors.first('simpleInput') }}</small>
</div>
<div class="form-check">
@ -330,57 +329,54 @@
v-model="srvModel.fitButtonInline"
v-on:change="inputChanges"
:class="{'is-invalid': errors.has('fitButtonInline') }">
<label class="form-check-label" for="fitButtonInline">Fit button inline</label>
<label class="form-check-label" for="fitButtonInline" text-translate="true">Fit button inline</label>
<small class="text-danger">{{ errors.first('fitButtonInline') }}</small>
</div>
</template>
</div>
<div class="col-xl-4 mt-4 mt-xl-0">
<h5 class="mb-3">Preview</h5>
<h5 class="mb-3" text-translate="true">Preview</h5>
<div id="preview"></div>
</div>
</div>
<h4 class="mt-5 mb-3">Payment Notifications</h4>
<h4 class="mt-5 mb-3" text-translate="true">Payment Notifications</h4>
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<div class="form-group">
<label class="form-label" for="server-ipn">Server IPN</label>
<label class="form-label" for="server-ipn" text-translate="true">Server IPN</label>
<input name="serverIpn" type="text" class="form-control" id="server-ipn"
v-model="srvModel.serverIpn" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('serverIpn') }">
<small class="text-danger">{{ errors.first('serverIpn') }}</small>
<div class="form-text">The URL to post purchase data.</div>
<div class="form-text" text-translate="true">The URL to post purchase data.</div>
</div>
<div class="form-group" v-if="!srvModel.appIdEndpoint">
<label class="form-label" for="email-notifications">Email Notifications</label>
<label class="form-label" for="email-notifications" text-translate="true">Email Notifications</label>
<input name="notifyEmail" type="text" class="form-control" id="email-notifications"
placeholder="name@domain.com"
placeholder="@StringLocalizer["user@example.com"]"
v-model="srvModel.notifyEmail" v-on:change="inputChanges"
v-validate="'email'" :class="{'is-invalid': errors.has('notifyEmail') }">
<small class="text-danger">{{ errors.first('notifyEmail') }}</small>
<div class="form-text">Receive email notification updates.</div>
<div class="form-text" text-translate="true">Receive email notification updates.</div>
</div>
<div class="form-group">
<label class="form-label" for="browser-redirect">Browser Redirect</label>
<label class="form-label" for="browser-redirect" text-translate="true">Browser Redirect</label>
<input name="browserRedirect" type="text" class="form-control" id="browser-redirect"
v-model="srvModel.browserRedirect" v-on:change="inputChanges"
v-validate="'url'" :class="{'is-invalid': errors.has('browserRedirect') }">
<small class="text-danger">{{ errors.first('browserRedirect') }}</small>
<div class="form-text">Where to redirect the customer after payment is complete</div>
<div class="form-text" text-translate="true">Where to redirect the customer after payment is complete</div>
</div>
</div>
</div>
<h4 class="mt-5 mb-3">Advanced Options</h4>
<h4 class="mt-5 mb-3" text-translate="true">Advanced Options</h4>
<div class="row" v-if="!srvModel.appIdEndpoint">
<div class="col-xl-8 col-xxl-constrain">
<p>
Specify additional query string parameters that should be appended to the checkout page once the invoice is created.
For example, <code>lang=da-DK</code> would load the checkout page in Danish by default.
</p>
<p>@ViewLocalizer["Specify additional query string parameters that should be appended to the checkout page once the invoice is created. For example, <code>lang=da-DK</code> would load the checkout page in Danish by default."]</p>
<div class="form-group">
<label class="form-label" for="query-string">Checkout Additional Query String</label>
<label class="form-label" for="query-string" text-translate="true">Checkout Additional Query String</label>
<input name="checkoutQueryString" type="text" class="form-control" id="query-string"
v-model="srvModel.checkoutQueryString" v-on:change="inputChanges"
:class="{'is-invalid': errors.has('checkoutQueryString') }">
@ -391,17 +387,17 @@
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<p>Link this Pay Button to an app instead. Some features are disabled due to the different endpoint capabilities. You can set which perk/item this button should be targeting.</p>
<p text-translate="true">Link this Pay Button to an app instead. Some features are disabled due to the different endpoint capabilities. You can set which perk/item this button should be targeting.</p>
<div class="form-group">
<label class="form-label" for="app-as-endpoint">Use App As Endpoint</label>
<label class="form-label" for="app-as-endpoint" text-translate="true">Use App As Endpoint</label>
<select v-model="srvModel.appIdEndpoint" v-on:change="inputChanges" class="form-select" id="app-as-endpoint">
<option value="">Use default pay button endpoint</option>
<option value="" text-translate="true">Use default pay button endpoint</option>
<option v-for="app in srvModel.apps" v-bind:value="app.id" >{{app.appName}} ({{app.appType}})</option>
</select>
<small class="text-danger">{{ errors.first('appIdEndpoint') }}</small>
</div>
<div class="form-group" v-if="srvModel.appIdEndpoint">
<label class="form-label" for="app-item">App Item/Perk</label>
<label class="form-label" for="app-item" text-translate="true">App Item/Perk</label>
<input name="appChoiceKey" type="text" class="form-control" id="app-item"
v-model="srvModel.appChoiceKey" v-on:change="inputChanges"
:class="{'is-invalid': errors.has('appChoiceKey') }">
@ -410,57 +406,61 @@
</div>
</div>
<h4 class="mt-5 mb-3">Generated Code</h4>
<h4 class="mt-5 mb-3" text-translate="true">Generated Code</h4>
<div class="row" v-show="!errors.any()">
<div class="col-xxl-8">
<pre><code id="mainCode" class="html"></code></pre>
<button class="btn btn-outline-secondary" data-clipboard-target="#mainCode">
<vc:icon symbol="actions-copy"/>&nbsp;Copy Code
<vc:icon symbol="actions-copy"/>&nbsp;<span text-translate="true">Copy Code</span>
</button>
</div>
</div>
<div class="row" v-show="errors.any()">
<div class="col-xl-8 col-xxl-constrain text-danger">
<div class="col-xl-8 col-xxl-constrain text-danger" text-translate="true">
Please fix errors shown in order for code generation to successfully execute.
</div>
</div>
<div v-if="!srvModel.appIdEndpoint && (previewLink || lnurlLink)">
<h4 class="mt-4 mb-3">Alternatives</h4>
<p>You can also share the link/LNURL or encode it in a QR code.</p>
<h4 class="mt-5 mb-3" text-translate="true">Alternatives</h4>
<p text-translate="true">You can also share the link/LNURL or encode it in a QR code.</p>
<div class="align-items-center" style="width:256px">
<ul class="nav my-3 btcpay-pills align-items-center gap-2">
<li class="nav-item" v-if="previewLink">
<a class="btcpay-pill" :class="{ active: alternativeMode === 'link' }" data-bs-toggle="tab" data-bs-target="#Alternative-Link" role="tab" href="#Alternative-Link">
<a class="btcpay-pill" :class="{ active: alternativeMode === 'link' }" data-bs-toggle="tab" data-bs-target="#Alternative-Link" role="tab" href="#Alternative-Link" text-translate="true">
Link
</a>
</li>
<li class="nav-item" v-if="previewLink">
<a class="btcpay-pill" :class="{ active: alternativeMode === 'lnurl' }" data-bs-toggle="tab" data-bs-target="#Alternative-LNURL" role="tab" href="#Alternative-LNURL">
<a class="btcpay-pill" :class="{ active: alternativeMode === 'lnurl' }" data-bs-toggle="tab" data-bs-target="#Alternative-LNURL" role="tab" href="#Alternative-LNURL" text-translate="true">
LNURL
</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane" :class="{ active: alternativeMode === 'link' }" id="Alternative-Link" role="tabpanel">
<a class="qr-container d-inline-block" :class="{ active: true }" :href="previewLink">
<qrcode :value="previewLink" :options="qrOptions" tag="img"></qrcode>
</a>
<div class="input-group mt-3">
<div class="form-floating">
<vc:truncate-center text="previewLink" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<label>Link URL</label>
<div class="payment-box">
<a class="qr-container d-inline-block" :class="{ active: true }" :href="previewLink">
<qrcode :value="previewLink" :options="qrOptions" tag="img"></qrcode>
</a>
<div class="input-group mt-3">
<div class="form-floating">
<vc:truncate-center text="previewLink" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<label text-translate="true">Link URL</label>
</div>
</div>
</div>
</div>
<div class="tab-pane" :class="{ active: alternativeMode === 'lnurl' }" id="Alternative-LNURL" role="tabpanel">
<a class="qr-container d-inline-block" :href="lnurlLink">
<qrcode :value="lnurlLink" :options="qrOptions" tag="img"></qrcode>
</a>
<div class="input-group mt-3">
<div class="form-floating">
<vc:truncate-center text="lnurlLink" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<label>LNURL</label>
<div class="payment-box">
<a class="qr-container d-inline-block" :href="lnurlLink">
<qrcode :value="lnurlLink" :options="qrOptions" tag="img"></qrcode>
</a>
<div class="input-group mt-3">
<div class="form-floating">
<vc:truncate-center text="lnurlLink" is-vue="true" padding="15" elastic="true" classes="form-control-plaintext" />
<label text-translate="true">LNURL</label>
</div>
</div>
</div>
</div>

View File

@ -46,7 +46,7 @@
</div>
@if (Model.ShowSearch)
{
<input id="SearchTerm" class="form-control rounded-pill" placeholder="Search…" v-model="searchTerm" v-if="showSearch">
<input id="SearchTerm" class="form-control rounded-pill" placeholder="@StringLocalizer["Search…"]" v-model="searchTerm" v-if="showSearch">
}
@if (Model.ShowCategories)
{
@ -97,7 +97,14 @@
@if (item.Inventory.HasValue)
{
<span class="badge text-bg-warning inventory" v-text="inventoryText(@index)">
@(item.Inventory > 0 ? $"{item.Inventory} left" : "Sold out")
@if (item.Inventory > 0)
{
<span>@ViewLocalizer["{0} left", item.Inventory.ToString()]</span>
}
else
{
<span text-translate="true">Sold out</span>
}
</span>
}
</div>
@ -113,7 +120,7 @@
{
<div class="input-group mb-2">
<span class="input-group-text">@Model.CurrencySymbol</span>
<input class="form-control" type="number" min="@(item.Price ?? 0)" step="@Model.Step" name="amount" placeholder="Amount" value="@item.Price" required v-on:click.stop>
<input class="form-control" type="number" min="@(item.Price ?? 0)" step="@Model.Step" name="amount" placeholder="@StringLocalizer["Amount"]" value="@item.Price" required v-on:click.stop>
</div>
}
<button type="button" class="btn btn-primary w-100" :disabled="!inStock(@index)">
@ -138,10 +145,10 @@
<div class="public-page-wrap" v-cloak>
<header class="sticky-top bg-tile offcanvas-header py-3 py-lg-4 d-flex align-items-baseline justify-content-center gap-3 px-5 pe-lg-0">
<h1 class="mb-0" id="cartLabel">Cart</h1>
<button id="CartClear" type="reset" v-on:click="clear" class="btn btn-text text-primary p-1" v-if="cartCount > 0">
<button id="CartClear" type="reset" v-on:click="clear" class="btn btn-text text-primary p-1" v-if="cartCount > 0" text-translate="true">
Empty
</button>
<button id="CartClose" type="button" class="cart-toggle-btn" v-on:click="toggleCart" aria-controls="cart" aria-label="Close">
<button id="CartClose" type="button" class="cart-toggle-btn" v-on:click="toggleCart" aria-controls="cart" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="cross" />
</button>
</header>
@ -156,7 +163,7 @@
<tr v-for="item in cart" :key="item.id">
<td class="align-middle">
<h6 class="fw-semibold mb-1">{{ item.title }}</h6>
<button type="button" v-on:click="removeFromCart(item.id)" class="btn btn-sm btn-link p-0 text-danger fw-semibold">Remove</button>
<button type="button" v-on:click="removeFromCart(item.id)" class="btn btn-sm btn-link p-0 text-danger fw-semibold" text-translate="true">Remove</button>
</td>
<td class="align-middle">
<div class="d-flex align-items-center gap-2 justify-content-end quantity">
@ -167,7 +174,7 @@
<button type="button" v-on:click="updateQuantity(item.id, -1)" class="btn btn-minus">
<span><vc:icon symbol="minus" /></span>
</button>
<input class="form-control hide-number-spin w-50px text-center" type="number" placeholder="Qty" min="1" step="1" :max="item.inventory" v-model.number="item.count">
<input class="form-control hide-number-spin w-50px text-center" type="number" placeholder="@StringLocalizer["Qty"]" min="1" step="1" :max="item.inventory" v-model.number="item.count">
<button type="button" v-on:click="updateQuantity(item.id, +1)" class="btn btn-plus">
<span><vc:icon symbol="plus" /></span>
</button>
@ -182,7 +189,7 @@
</table>
<table class="table table-borderless my-4" v-if="showDiscount || enableTips">
<tr v-if="showDiscount">
<th class="align-middle">Discount</th>
<th class="align-middle" text-translate="true">Discount</th>
<th class="align-middle" colspan="3">
<div class="input-group input-group-sm w-100px pull-right">
<input class="form-control hide-number-spin" type="number" min="0" step="1" max="100" id="Discount" v-model.number="discountPercent">
@ -191,7 +198,7 @@
</th>
</tr>
<tr v-if="enableTips">
<th class="align-middle">Tip</th>
<th class="align-middle" text-translate="true">Tip</th>
<th class="align-middle" colspan="3">
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-end gap-2" v-if="customTipPercentages">
<div class="btcpay-pill d-flex align-items-center px-3" id="Tip-Custom" :class="{ active: !tipPercent && tip }" v-on:click.prevent="tipPercent = null">
@ -219,25 +226,25 @@
</table>
<table class="table table-borderless mt-4 mb-0">
<tr>
<td class="align-middle">Subtotal</td>
<td class="align-middle" text-translate="true">Subtotal</td>
<td class="align-middle text-end" id="CartAmount">{{ formatCurrency(amountNumeric, true) }}</td>
</tr>
<tr v-if="discountNumeric">
<td class="align-middle">Discount</td>
<td class="align-middle" text-translate="true">Discount</td>
<td class="align-middle text-end" id="CartDiscount">
<span v-if="discountPercent">{{discountPercent}}% =</span>
{{ formatCurrency(discountNumeric, true) }}
</td>
</tr>
<tr v-if="tipNumeric">
<td class="align-middle">Tip</td>
<td class="align-middle" text-translate="true">Tip</td>
<td class="align-middle text-end" id="CartTip">
<span v-if="tipPercent">{{tipPercent}}% =</span>
{{ formatCurrency(tipNumeric, true) }}
</td>
</tr>
<tr>
<td class="align-middle h5 border-0">Total</td>
<td class="align-middle h5 border-0" text-translate="true">Total</td>
<td class="align-middle h5 border-0 text-end" id="CartTotal">{{ formatCurrency(totalNumeric, true) }}</td>
</tr>
<tr>
@ -246,13 +253,13 @@
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
<template v-else>Pay</template>
<template v-else text-translate="true">Pay</template>
</button>
</td>
</tr>
</table>
</form>
<p id="CartItems" v-else class="text-muted text-center my-0">There are no items in your cart yet.</p>
<p id="CartItems" v-else class="text-muted text-center my-0" text-translate="true">There are no items in your cart yet.</p>
</div>
</div>
</aside>

View File

@ -3,7 +3,7 @@
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false">
<div class="input-group">
<span class="input-group-text">@Model.CurrencySymbol</span>
<input class="form-control" type="number" step="@Model.Step" name="amount" placeholder="Amount">
<input class="form-control" type="number" step="@Model.Step" name="amount" placeholder="@StringLocalizer["Amount"]">
<button class="btn btn-primary" type="submit" text-translate="true">Pay</button>
</div>
</form>

View File

@ -101,7 +101,14 @@ else
@if (item.Inventory.HasValue)
{
<span class="badge text-bg-light">
@(item.Inventory > 0 ? $"{item.Inventory} left" : "Sold out")
@if (item.Inventory > 0)
{
<span>@ViewLocalizer["{0} left", item.Inventory.ToString()]</span>
}
else
{
<span text-translate="true">Sold out</span>
}
</span>
}
</div>

View File

@ -10,7 +10,7 @@
<vc:icon symbol="actions-refresh"/>
<span v-if="recentTransactionsLoading" class="visually-hidden" text-translate="true">Loading...</span>
</button>
<button type="button" class="btn-close py-3" aria-label="Close" v-on:click="hideRecentTransactions">
<button type="button" class="btn-close py-3" aria-label="@StringLocalizer["Close"]" v-on:click="hideRecentTransactions">
<vc:icon symbol="close"/>
</button>
</div>

View File

@ -57,7 +57,7 @@
<span class="badge text-bg-warning">
@if (item.Inventory > 0)
{
<span text-translate="true">@ViewLocalizer["{0} left", item.Inventory.ToString()]</span>
<span>@ViewLocalizer["{0} left", item.Inventory.ToString()]</span>
}
else
{
@ -80,7 +80,7 @@
{
<div class="input-group mb-2">
<span class="input-group-text">@Model.CurrencySymbol</span>
<input class="form-control" type="number" min="@(item.Price ?? 0)" step="@Model.Step" name="amount" placeholder="Amount" value="@item.Price" required>
<input class="form-control" type="number" min="@(item.Price ?? 0)" step="@Model.Step" name="amount" placeholder="@StringLocalizer["Amount"]" value="@item.Price" required>
</div>
}
<button class="btn btn-primary w-100" type="submit">@Safe.Raw(buttonText)</button>
@ -102,7 +102,7 @@
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" autocomplete="off">
<div class="input-group mb-2">
<span class="input-group-text">@Model.CurrencySymbol</span>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount" required>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="@StringLocalizer["Amount"]" required>
</div>
<button class="btn btn-primary w-100" type="submit">@Safe.Raw(Model.CustomButtonText ?? Model.ButtonText)</button>
</form>

View File

@ -65,7 +65,11 @@
</template>
</div>
<div class="keypad">
<button v-for="k in keys" :key="k" :disabled="k === '+' && mode !== 'amounts'" v-on:click.prevent="keyPressed(k)" v-on:dblclick.prevent="doubleClick(k)" type="button" class="btn btn-secondary btn-lg" :data-key="k">{{ k }}</button>
<button v-for="k in keys" :key="k" :disabled="k === '+' && mode !== 'amounts'" v-on:click.prevent="keyPressed(k)" v-on:dblclick.prevent="doubleClick(k)" type="button" class="btn btn-secondary btn-lg" :data-key="k">
<template v-if="k === 'C'"><vc:icon symbol="keypad-clear"/></template>
<template v-else-if="k === '+'"><vc:icon symbol="keypad-plus"/></template>
<template v-else>{{ k }}</template>
</button>
</div>
<button class="btn btn-lg btn-primary mx-3" type="submit" :disabled="payButtonLoading || totalNumeric <= 0" id="pay-button">
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
@ -89,7 +93,7 @@
@if (Model.ShowSearch)
{
<div class="w-100 mt-3">
<input id="SearchTerm" class="form-control rounded-pill" placeholder="Search…" v-model="searchTerm" v-if="showSearch">
<input id="SearchTerm" class="form-control rounded-pill" placeholder="@StringLocalizer["Search…"]" v-model="searchTerm" v-if="showSearch">
</div>
}
@if (Model.ShowCategories)
@ -135,7 +139,14 @@
@if (item.Inventory.HasValue)
{
<span class="badge text-bg-warning inventory" v-text="inventoryText(@index)">
@(item.Inventory > 0 ? $"{item.Inventory} left" : "Sold out")
@if (item.Inventory > 0)
{
<span>@ViewLocalizer["{0} left", item.Inventory.ToString()]</span>
}
else
{
<span text-translate="true">Sold out</span>
}
</span>
}
</div>

View File

@ -9,7 +9,7 @@
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
@model BTCPayServer.Plugins.PointOfSale.Models.UpdatePointOfSaleViewModel
@{
ViewData.SetActivePage(AppsNavPages.Update, "Update Point of Sale", Model.Id);
ViewData.SetActivePage(AppsNavPages.Update, StringLocalizer["Update Point of Sale"], Model.Id);
Csp.UnsafeEval();
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
var posPath = Url.Action("ViewPointOfSale", "UIPointOfSale", new { appId = Model.Id });
@ -34,12 +34,12 @@
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
@if (Model.Archived)
{
<button type="submit" class="btn btn-outline-secondary" name="Archived" value="False">Unarchive</button>
<button type="submit" class="btn btn-outline-secondary" name="Archived" value="False" text-translate="true">Unarchive</button>
}
else
{
<div class="btn-group" role="group" aria-label="View Point of Sale">
<a class="btn btn-secondary" asp-controller="UIPointOfSale" asp-action="ViewPointOfSale" asp-route-appId="@Model.Id" id="ViewApp" target="_blank">View</a>
<a class="btn btn-secondary" asp-controller="UIPointOfSale" asp-action="ViewPointOfSale" asp-route-appId="@Model.Id" id="ViewApp" target="_blank" text-translate="true">View</a>
<button type="button" class="btn btn-secondary px-3 d-inline-flex align-items-center" data-bs-toggle="modal" data-bs-target="#OpenPosModal">
<vc:icon symbol="qr-code" />
</button>
@ -76,7 +76,7 @@
</div>
</div>
<div class="form-group">
<label asp-for="DefaultView" class="form-label" data-required>Choose Point of Sale Style</label>
<label asp-for="DefaultView" class="form-label" data-required text-translate="true">Choose Point of Sale Style</label>
<div class="btcpay-list-select">
@foreach (var type in Enum.GetValues<PosViewType>())
{
@ -91,7 +91,7 @@
<div class="form-group mb-0">
<label asp-for="Currency" class="form-label"></label>
<input asp-for="Currency" class="form-control w-auto" currency-selection />
<div class="form-text">Uses the store's default currency (@Model.StoreDefaultCurrency) if empty.</div>
<div class="form-text">@StringLocalizer["Uses the store's default currency ({0}) if empty.", Model.StoreDefaultCurrency]</div>
<span asp-validation-for="Currency" class="text-danger"></span>
</div>
</div>
@ -112,7 +112,7 @@
</div>
<div class="row mt-5">
<div class="col-sm-10 col-md-9 col-xl-7 col-xxl-6">
<h3 class="mb-4">Checkout</h3>
<h3 class="mb-4" text-translate="true">Checkout</h3>
<fieldset>
<div class="form-group" id="button-price-text">
<label asp-for="ButtonText" class="form-label" data-required></label>
@ -126,7 +126,7 @@
</div>
</fieldset>
<fieldset id="keypad-display" class="mt-2">
<legend class="h5 mb-3 fw-semibold">Keypad</legend>
<legend class="h5 mb-3 fw-semibold" text-translate="true">Keypad</legend>
<div class="form-group d-flex align-items-center pt-2">
<input asp-for="ShowItems" type="checkbox" class="btcpay-toggle me-3" />
<label asp-for="ShowItems" class="form-label mb-0"></label>
@ -147,7 +147,7 @@
</div>
</fieldset>
<fieldset id="tips" class="mt-2">
<legend class="h5 mb-3 fw-semibold">Tips</legend>
<legend class="h5 mb-3 fw-semibold" text-translate="true">Tips</legend>
<div class="form-group d-flex align-items-center pt-2">
<input asp-for="EnableTips" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#CustomTipsSettings" aria-expanded="@Model.EnableTips" aria-controls="CustomTipsSettings" />
<label asp-for="EnableTips" class="form-check-label"></label>
@ -167,18 +167,18 @@
</div>
</fieldset>
<fieldset id="discounts" class="mt-2">
<legend class="h5 mb-3 fw-semibold">Discounts</legend>
<legend class="h5 mb-3 fw-semibold" text-translate="true">Discounts</legend>
<div class="form-group d-flex align-items-center">
<input asp-for="ShowDiscount" type="checkbox" class="btcpay-toggle me-3" />
<div>
<label asp-for="ShowDiscount" class="form-check-label"></label>
<div class="text-muted">Not recommended for customer self-checkout.</div>
<div class="text-muted" text-translate="true">Not recommended for customer self-checkout.</div>
</div>
<span asp-validation-for="ShowDiscount" class="text-danger"></span>
</div>
</fieldset>
<fieldset id="custom-payments" class="mt-2">
<legend class="h5 mb-3 fw-semibold">Custom Payments</legend>
<legend class="h5 mb-3 fw-semibold" text-translate="true">Custom Payments</legend>
<div class="form-group mb-4 d-flex align-items-center">
<input asp-for="ShowCustomAmount" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#CustomAmountSettings" aria-expanded="@Model.ShowCustomAmount" aria-controls="CustomAmountSettings"/>
<label asp-for="ShowCustomAmount" class="form-check-label"></label>
@ -202,21 +202,21 @@
<div class="accordion-item">
<h2 class="accordion-header" id="additional-embed-payment-button-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-embed-payment-button" aria-expanded="false" aria-controls="additional-embed-payment-button">
Embed a payment button linking to POS item
<span text-translate="true">Embed a payment button linking to POS item</span>
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-embed-payment-button" class="accordion-collapse collapse" aria-labelledby="additional-embed-payment-button-header">
<div class="accordion-body">
<p>You can host point of sale buttons in an external website with the following code.</p>
<p text-translate="true">You can host point of sale buttons in an external website with the following code.</p>
@if (Model.Example1 != null)
{
<span>For anything with a custom amount</span>
<span text-translate="true">For anything with a custom amount</span>
<pre class="p-3">@Model.Example1</pre>
}
@if (Model.Example2 != null)
{
<span>For a specific item of your template</span>
<span text-translate="true">For a specific item of your template</span>
<pre class="p-3">@Model.Example2</pre>
}
</div>
@ -225,13 +225,13 @@
<div class="accordion-item">
<h2 class="accordion-header" id="additional-embed-pos-iframe-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-embed-pos-iframe" aria-expanded="false" aria-controls="additional-embed-pos-iframe">
Embed Point of Sale via Iframe
<span text-translate="true">Embed Point of Sale via iframe</span>
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-embed-pos-iframe" class="accordion-collapse collapse" aria-labelledby="additional-embed-pos-iframe-header">
<div class="accordion-body">
You can embed this POS via an iframe.
<span text-translate="true">You can embed this POS via an iframe.</span>
@{
var iframe = $"<iframe src='{Url.Action("ViewPointOfSale", "UIPointOfSale", new { appId = Model.Id }, Context.Request.Scheme)}' style='max-width: 100%; border: 0;'></iframe>";
}
@ -242,7 +242,7 @@
<div class="accordion-item">
<h2 class="accordion-header" id="additional-redirect-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-redirect" aria-expanded="false" aria-controls="additional-redirect">
Redirects
<span text-translate="true">Redirects</span>
<vc:icon symbol="caret-down" />
</button>
</h2>
@ -264,7 +264,7 @@
<div class="accordion-item">
<h2 class="accordion-header" id="additional-notification-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-notification" aria-expanded="false" aria-controls="additional-notification">
Notification URL Callbacks
<span text-translate="true">Notification URL Callbacks</span>
<vc:icon symbol="caret-down" />
</button>
</h2>
@ -275,13 +275,13 @@
<input asp-for="NotificationUrl" class="form-control" />
<span asp-validation-for="NotificationUrl" class="text-danger"></span>
</div>
<p>A <code>POST</code> callback will be sent to the specified <code>notificationUrl</code> (for on-chain transactions when there are sufficient confirmations):</p>
<p html-translate="true">A <code>POST</code> callback will be sent to the specified <code>notificationUrl</code> (for on-chain transactions when there are sufficient confirmations):</p>
<pre class="p-3">@Model.ExampleCallback</pre>
<p><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<p html-translate="true"><strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:</p>
<ul>
<li>Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json; Authorization: Basic YourLegacyAPIkey"</code>, Legacy API key can be created with Access Tokens in Store settings</li>
<li>Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code></li>
<li>You can then ship your order</li>
<li html-translate="true">Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json; Authorization: Basic YourLegacyAPIkey"</code>, Legacy API key can be created with Access Tokens in Store settings</li>
<li html-translate="true">Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code></li>
<li text-translate="true">You can then ship your order</li>
</ul>
</div>
</div>
@ -293,30 +293,30 @@
</form>
<div class="d-grid d-sm-flex flex-wrap gap-3 mt-3">
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm">Invoices</a>
<a class="btn btn-secondary" asp-action="ListInvoices" asp-controller="UIInvoice" asp-route-storeId="@Model.StoreId" asp-route-searchterm="@Model.SearchTerm" text-translate="true">Invoices</a>
<form method="post" asp-controller="UIApps" asp-action="ToggleArchive" asp-route-appId="@Model.Id">
<button type="submit" class="w-100 btn btn-outline-secondary" id="btn-archive-toggle" permission="@Policies.CanModifyStoreSettings">
@if (Model.Archived)
{
<span class="text-nowrap">Unarchive this app</span>
<span class="text-nowrap" text-translate="true">Unarchive this app</span>
}
else
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this app so that it does not appear in the apps list by default">Archive this app</span>
<span class="text-nowrap" data-bs-toggle="tooltip" title="@StringLocalizer["Archive this app so that it does not appear in the apps list by default"]" text-translate="true">Archive this app</span>
}
</button>
</form>
<a id="DeleteApp" class="btn btn-outline-danger" asp-controller="UIApps" asp-action="DeleteApp" asp-route-appId="@Model.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@Html.Encode(Model.AppName)</strong> and its settings will be permanently deleted." data-confirm-input="DELETE" permission="@Policies.CanModifyStoreSettings">Delete this app</a>
<a id="DeleteApp" class="btn btn-outline-danger" asp-controller="UIApps" asp-action="DeleteApp" asp-route-appId="@Model.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="@StringLocalizer["The app <strong>{0}</strong> and its settings will be permanently deleted.", Html.Encode(Model.AppName)]" data-confirm-input="@StringLocalizer["DELETE"]" permission="@Policies.CanModifyStoreSettings" text-translate="true">Delete this app</a>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Delete app", "This app will be removed from this store.", "Delete"))" permission="@Policies.CanModifyStoreSettings" />
<partial name="_Confirm" model="@(new ConfirmModel(StringLocalizer["Delete app"], StringLocalizer["This app will be removed from this store."], StringLocalizer["Delete"]))" permission="@Policies.CanModifyStoreSettings" />
<div class="modal fade" id="OpenPosModal" tabindex="-1" aria-labelledby="ConfirmTitle" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Scan the QR code to open the Point of Sale</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<h5 class="modal-title" text-translate="true">Scan the QR code to open the Point of Sale</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close"/>
</button>
</div>

View File

@ -4,7 +4,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{title}} <template v-if="fragments && fragments.length > 1">({{index+1}}/{{fragments.length}})</template></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close"/>
</button>
</div>

View File

@ -22,7 +22,7 @@
<label for="EditorId" class="form-label" data-required>ID</label>
<input id="EditorId" required class="form-control mb-2" v-model="editingItem && editingItem.id" v-on:change="onIdChange" />
<div class="text-danger mb-3" v-if="errors.id">{{errors.id}}</div>
<div class="form-text" v-else>Leave blank to generate ID from title.</div>
<div class="form-text" v-else text-translate="true">Leave blank to generate ID from title.</div>
</div>
<div class="form-group row">
<div class="col-sm-6">
@ -51,35 +51,35 @@
<div class="text-danger" v-if="errors.price">{{errors.price}}</div>
</div>
<div class="form-group">
<label for="EditorImageUrl" class="form-label">Image Url</label>
<label for="EditorImageUrl" class="form-label" text-translate="true">Image Url</label>
<input id="EditorImageUrl" class="form-control mb-2" v-model="editingItem && editingItem.image" ref="txtImage" />
<item-editor-upload upload-url=@Safe.Json(Url.Action("FileUpload", "UIApps", new { appId = Context.GetRouteValue("appId") })) v-on:uploaded="url => editingItem.image = url" />
</div>
<div class="form-group">
<label for="EditorDescription" class="form-label">Description</label>
<label for="EditorDescription" class="form-label" text-translate="true">Description</label>
<textarea id="EditorDescription" rows="3" cols="40" class="form-control" v-model="editingItem && editingItem.description"></textarea>
</div>
<div class="form-group">
<label for="EditorCategories" class="form-label">Categories</label>
<label for="EditorCategories" class="form-label" text-translate="true">Categories</label>
<input id="EditorCategories" class="form-control mb-2" autocomplete="off" ref="editorCategories" />
<div class="form-text">Easily filter the different items using categories, used only in the product list with cart.</div>
<div class="form-text" text-translate="true">Easily filter the different items using categories, used only in the product list with cart.</div>
</div>
<div class="form-group">
<label for="EditorInventory" class="form-label">Inventory</label>
<label for="EditorInventory" class="form-label" text-translate="true">Inventory</label>
<input id="EditorInventory" type="number" inputmode="numeric" min="0" step="1" class="form-control mb-2" v-model="editingItem && editingItem.inventory" v-on:change="onInventoryChange" />
<div class="form-text">Leave blank to not use this feature.</div>
<div class="form-text" text-translate="true">Leave blank to not use this feature.</div>
</div>
<div class="form-group">
<label for="BuyButtonText" class="form-label">Buy Button Text</label>
<label for="BuyButtonText" class="form-label" text-translate="true">Buy Button Text</label>
<input id="BuyButtonText" type="text" class="form-control mb-2" v-model="editingItem && editingItem.buyButtonText" />
</div>
<div class="form-group d-flex align-items-center">
<input type="checkbox" id="Disabled" class="btcpay-toggle me-3" :checked="editingItem && !editingItem.disabled" v-on:change="$event => editingItem.disabled = !$event.target.checked" />
<label for="Disabled" class="form-check-label">Enable</label>
<label for="Disabled" class="form-check-label" text-translate="true">Enable</label>
</div>
<vc:ui-extension-point location="app-template-editor-item-detail" model="Model"></vc:ui-extension-point>
</div>
<div v-if="!editingItem">Select an item to edit</div>
<div v-if="!editingItem" text-translate="true">Select an item to edit</div>
</div>
</template>
@ -112,7 +112,7 @@
</div>
<button type="button" id="btAddItem" 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-item', $event)">
<vc:icon symbol="actions-add" />
Add Item
<span text-translate="true">Add Item</span>
</button>
</div>
</template>
@ -129,10 +129,10 @@
<div class="d-flex flex-wrap align-items-end justify-content-between gap-3 mb-3">
<ul class="nav nav-pills gap-4" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="EditorTabButton" data-bs-toggle="pill" data-bs-target="#EditorTabPane" type="button" role="tab" aria-controls="EditorTabPane" aria-selected="true">Editor</button>
<button class="nav-link active" id="EditorTabButton" data-bs-toggle="pill" data-bs-target="#EditorTabPane" type="button" role="tab" aria-controls="EditorTabPane" aria-selected="true" text-translate="true">Editor</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="CodeTabButton" data-bs-toggle="pill" data-bs-target="#CodeTabPane" type="button" role="tab" aria-controls="CodeTabPane" aria-selected="false">Code</button>
<button class="nav-link" id="CodeTabButton" data-bs-toggle="pill" data-bs-target="#CodeTabPane" type="button" role="tab" aria-controls="CodeTabPane" aria-selected="false" text-translate="true">Code</button>
</li>
</ul>
</div>
@ -151,7 +151,7 @@
</div>
<div class="col-xl-5 offcanvas-xl offcanvas-end" tabindex="-1" ref="editorOffcanvas">
<div class="offcanvas-header justify-content-between p-3">
<h5 class="offcanvas-title">Edit Item</h5>
<h5 class="offcanvas-title" text-translate="true">Edit Item</h5>
<button type="button" class="btn btn-sm rounded-pill" :class="{ 'btn-primary': itemChanged, 'btn-outline-secondary': !itemChanged }" v-on:click="hideOffcanvas" v-text="itemChanged ? 'Apply' : 'Close'"></button>
</div>
<div class="offcanvas-body p-3 p-xl-0">
@ -161,7 +161,7 @@
</div>
</div>
<div class="tab-pane fade" id="CodeTabPane" role="tabpanel" aria-labelledby="CodeTabButton" tabindex="0">
<label for="TemplateConfig" class="form-label">Template JSON</label>
<label for="TemplateConfig" class="form-label" text-translate="true">Template JSON</label>
<textarea id="TemplateConfig" name="@Model.templateId" rows="21" cols="21" class="form-control font-monospace" style="font-size:.85rem" v-model="itemsJSON" v-on:change="updateFromJSON">@Model.template</textarea>
<span asp-validation-for="@Model.templateId" class="text-danger"></span>
</div>

View File

@ -48,19 +48,19 @@
</div>
<div id="passphrase-input" class="mt-4" style="display: none;">
<div class="form-group">
<label for="Password" class="form-label">Passphrase (Leave empty if there isn't any passphrase)</label>
<label for="Password" class="form-label" text-translate="true">Passphrase (Leave empty if there isn't any passphrase)</label>
<div class="input-group">
<input id="Password" type="password" class="form-control">
<button type="button" class="btn btn-secondary px-3 only-for-js" title="Toggle passphrase visibility" data-toggle-password="#Password">
<button type="button" class="btn btn-secondary px-3 only-for-js" title="@StringLocalizer["Toggle passphrase visibility"]" data-toggle-password="#Password">
<vc:icon symbol="actions-show" />
</button>
</div>
</div>
<div class="form-group">
<label for="PasswordConfirmation" class="form-label">Passphrase confirmation</label>
<label for="PasswordConfirmation" class="form-label" text-translate="true">Passphrase confirmation</label>
<div class="input-group">
<input id="PasswordConfirmation" type="password" class="form-control">
<button type="button" class="btn btn-secondary px-3 only-for-js" title="Toggle passphrase visibility" data-toggle-password="#PasswordConfirmation">
<button type="button" class="btn btn-secondary px-3 only-for-js" title="@StringLocalizer["Toggle passphrase visibility"]" data-toggle-password="#PasswordConfirmation">
<vc:icon symbol="actions-show" />
</button>
</div>

View File

@ -1,4 +1,4 @@
<h5 class="text-center fw-normal mb-4">
<h5 class="text-center fw-normal mb-4" text-translate="true">
BTCPay Server Supporters
</h5>
@ -58,6 +58,7 @@
</div>
</div>
<p class="text-center">
<a href="https://foundation.btcpayserver.org" target="_blank" rel="noreferrer noopener">View all supporters</a> or
<a href="https://btcpayserver.org/donate/" target="_blank" rel="noreferrer noopener">Donate</a>
<a href="https://foundation.btcpayserver.org" target="_blank" rel="noreferrer noopener" text-translate="true">View all supporters</a>
<span text-translate="true">or</span>
<a href="https://btcpayserver.org/donate/" target="_blank" rel="noreferrer noopener" text-translate="true">Donate</a>
</p>

View File

@ -21,21 +21,21 @@
</a>
<a href="https://btcpayserver.org/donate/" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="donate"/>
<span>Donate</span>
<span text-translate="true">Donate</span>
</a>
<a asp-controller="UIHome" asp-action="SwaggerDocs" target="_blank">
<vc:icon symbol="api"/>
<span>API</span>
<span text-translate="true">API</span>
</a>
<a href="https://docs.btcpayserver.org/" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="docs"/>
<span>Docs</span>
<span text-translate="true">Docs</span>
</a>
@if (!string.IsNullOrEmpty(_env.OnionUrl) && !Context.Request.IsOnion())
{
<button type="button" class="d-flex align-items-center btn btn-link" data-clipboard="@_env.OnionUrl" data-clipboard-hover>
<vc:icon symbol="logo-tor"/>
<span>Copy Tor URL</span>
<span text-translate="true">Copy Tor URL</span>
</button>
}
</div>
@ -54,7 +54,7 @@
<div class="toast-header">
<span class="blazor-status__state btcpay-status"></span>
<h6 class="blazor-status__title ms-2 mb-0 me-auto"></h6>
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
</div>

View File

@ -34,27 +34,21 @@
</header>
<template id="badUrl">
<div class="alert alert-danger alert-dismissible m-3" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close"/>
</button>
<span>
BTCPay is expecting you to access this website from <strong>@(expectedScheme)://@(expectedHost)/</strong>.
If you use a reverse proxy, please set the <strong>X-Forwarded-Proto</strong> header to <strong id="browserScheme">@(expectedScheme)</strong>
(<a href="https://docs.btcpayserver.org/FAQ/Deployment/#cause-3-btcpay-is-expecting-you-to-access-this-website-from" target="_blank" class="alert-link" rel="noreferrer noopener">More information</a>)
</span>
<span html-translate="true">BTCPay is expecting you to access this website from <strong>@(expectedScheme)://@(expectedHost)/</strong>. If you use a reverse proxy, please set the <strong>X-Forwarded-Proto</strong> header to <strong id="browserScheme">@(expectedScheme)</strong></span>
(<a href="https://docs.btcpayserver.org/FAQ/Deployment/#cause-3-btcpay-is-expecting-you-to-access-this-website-from" target="_blank" class="alert-link" rel="noreferrer noopener" text-translate="true">More information</a>)
</div>
</template>
<main id="mainContent">
@if (!_env.IsSecure(_context.HttpContext))
{
<div id="insecureEnv" class="alert alert-danger alert-dismissible" style="position:absolute; top:75px;" role="alert">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close"/>
</button>
<span>
Your access to BTCPay Server is over an unsecured network. If you are using the docker deployment method with NGINX and HTTPS is not available, you probably did not configure your DNS settings correctly. <br/>
We disabled the register and login link so you don't leak your credentials.
</span>
<span text-translate="true">Your access to BTCPay Server is over an unsecured network. If you are using the docker deployment method with NGINX and HTTPS is not available, you probably did not configure your DNS settings correctly. We disabled the register and login link so you don't leak your credentials.</span>
</div>
}
<section>

View File

@ -7,7 +7,7 @@
<div class="alert alert-@parsedModel.SeverityCSS @(parsedModel.AllowDismiss? "alert-dismissible":"" ) @(ViewData["Margin"] ?? "mb-4") text-break" role="alert" v-pre>
@if (parsedModel.AllowDismiss)
{
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
}

View File

@ -11,10 +11,7 @@
@if (isEmailConfigured)
{
<p text-translate="true">
We all forget passwords every now and then. Just provide email address tied to
your account and we'll start the process of helping you recover your account.
</p>
<p text-translate="true">We all forget passwords sometimes. Just provide email address tied to your account, and we'll start the process of helping you recover your account.</p>
<form asp-action="ForgotPassword" method="post">
@if (!ViewContext.ModelState.IsValid)

View File

@ -36,7 +36,7 @@
<div class="form-group mt-4">
<div class="btn-group w-100">
<button type="submit" class="btn btn-primary btn-lg w-100" id="LoginButton"><span class="ps-3" text-translate="true" text-translate="true">Sign in</span></button>
<button type="button" class="btn btn-outline-primary btn-lg w-auto only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan Login code with camera">
<button type="button" class="btn btn-outline-primary btn-lg w-auto only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="@StringLocalizer["Scan Login code with camera"]">
<vc:icon symbol="scan-qr" />
</button>
</div>

View File

@ -4,10 +4,7 @@
ViewData["Title"] = "Recovery code verification";
}
<p text-translate="true">
You have requested to login with a recovery code. This login will not be remembered until you provide
an authenticator app code at login or disable 2FA and login again.
</p>
<p text-translate="true">You have requested to log in with a recovery code. This login will not be remembered until you provide an authenticator app code at login or disable 2FA and log in again.</p>
<form method="post">
<div class="form-group">
<label asp-for="RecoveryCode" class="form-label"></label>

View File

@ -36,7 +36,7 @@
<h2>
@ViewData["Title"]
<small>
<a href="https://docs.btcpayserver.org/Apps/" target="_blank" rel="noreferrer noopener" title="More information...">
<a href="https://docs.btcpayserver.org/Apps/" target="_blank" rel="noreferrer noopener" title="@StringLocalizer["More information..."]">
<vc:icon symbol="info" />
</a>
</small>

View File

@ -1,6 +1,6 @@
@model Fido2NetLib.CredentialCreateOptions
@{
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, "Register your security device");
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, StringLocalizer["Register your security device"]);
}
<div class="sticky-header">

View File

@ -3,18 +3,18 @@
@using BTCPayServer.Client
@model List<BTCPayServer.Data.FormData>
@{
ViewData.SetActivePage(StoreNavPages.Forms, "Forms");
ViewData.SetActivePage(StoreNavPages.Forms, StringLocalizer["Forms"]);
var storeId = Context.GetCurrentStoreId();
}
<div class="sticky-header">
<h2>
<span text-translate="true">@ViewData["Title"]</span>
<a href="https://docs.btcpayserver.org/Forms" target="_blank" rel="noreferrer noopener" title="More information...">
<a href="https://docs.btcpayserver.org/Forms" target="_blank" rel="noreferrer noopener" title="@StringLocalizer["More information..."]">
<vc:icon symbol="info" />
</a>
</h2>
<a id="page-primary" asp-action="Create" asp-route-storeId="@storeId" class="btn btn-primary mt-3 mt-sm-0" role="button" permission="@Policies.CanModifyStoreSettings">
<a id="page-primary" asp-action="Create" asp-route-storeId="@storeId" class="btn btn-primary mt-3 mt-sm-0" role="button" permission="@Policies.CanModifyStoreSettings" text-translate="true">
Create Form
</a>
</div>

View File

@ -4,10 +4,10 @@
@model BTCPayServer.Forms.ModifyForm
@{
Csp.UnsafeEval();
var storeId = Context.GetCurrentStoreId();
var formId = Context.GetRouteValue("id");
var isNew = formId is null;
ViewData.SetActivePage(StoreNavPages.Forms, $"{(isNew ? "Create" : "Edit")} Form", Model.Name);
var storeId = Context.GetCurrentStoreId();
ViewData.SetActivePage(StoreNavPages.Forms, isNew ? StringLocalizer["Create Form"] : StringLocalizer["Edit Form"], Model.Name);
}
@section PageHeadContent {
@ -30,17 +30,17 @@
<template id="field-editor">
<div class="field" v-if="field">
<div class="form-group">
<label for="field-editor-field-type" class="form-label" data-required>Type</label>
<label for="field-editor-field-type" class="form-label" data-required text-translate="true">Type</label>
<select id="field-editor-field-type" class="form-select" required v-model="field.type">
<option v-for="option in fieldTypeOptions" :key="option" :value="option" v-text="option.charAt(0).toUpperCase() + option.slice(1)"></option>
</select>
</div>
<div class="form-group">
<label for="field-editor-field-label" class="form-label" data-required>Label</label>
<label for="field-editor-field-label" class="form-label" data-required text-translate="true">Label</label>
<input id="field-editor-field-label" class="form-control" required v-model="field.label" />
</div>
<div class="form-group">
<label for="field-editor-field-name" class="form-label" data-required>Name</label>
<label for="field-editor-field-name" class="form-label" data-required text-translate="true">Name</label>
<input id="field-editor-field-name" class="form-control" list="special-field-names" required v-model="field.name" />
<div text-translate="true" class="form-text">The name of the field in the invoice's metadata.</div>
<div text-translate="true" class="form-text text-info" v-if="field.name === 'invoice_currency'">The configured name means the value of this field will determine the invoice currency for public forms.</div>
@ -56,11 +56,11 @@
<vc:icon symbol="drag" />
</button>
<div class="field flex-grow-1">
<label :for="`field-option-value-${index}`" class="form-label">Value</label>
<label :for="`field-option-value-${index}`" class="form-label" text-translate="true">Value</label>
<input :id="`field-option-value-${index}`" class="form-control" v-model.lazy="option.value" />
</div>
<div class="field flex-grow-1">
<label :for="`field-option-text-${index}`" class="form-label">Text</label>
<label :for="`field-option-text-${index}`" class="form-label" text-translate="true">Text</label>
<input :id="`field-option-text-${index}`" class="form-control" v-model="option.text" />
</div>
<button type="button" class="btn b-0 control remove" v-on:click="removeOption($event, index)">
@ -90,15 +90,15 @@
<div class="options">
<div v-if="field.valuemap" v-for="(v, k, index) in field.valuemap" :key="k" class="d-flex align-items-start gap-2 pt-3">
<div class="field flex-grow-1">
<label :for="`field-valuemap-value-${index}`" class="form-label">Original Value</label>
<label :for="`field-valuemap-value-${index}`" class="form-label" text-translate="true">Original Value</label>
<select v-if="mirroredField && mirroredField.type === 'select'" :id="`field-valuemap-value-${index}`" class="form-select" v-on:change="updateValueMap(k, $event.target.value, v)">
<option v-for="option in mirroredField.options" v-if="option.text && option.value" :key="option.value" :value="option.value" :selected="k === option.value" v-text="`${option.value} (${option.text})`"></option>
</select>
<input v-else :id="`field-valuemap-value-${index}`" class="form-control" placeholder="Value to match" :value="k" v-on:change="updateValueMap(k, $event.target.value, v)" />
<input v-else :id="`field-valuemap-value-${index}`" class="form-control" placeholder="@StringLocalizer["Value to match"]" :value="k" v-on:change="updateValueMap(k, $event.target.value, v)" />
</div>
<div class="field flex-grow-1">
<label :for="`field-valuemap-mapped-${index}`" class="form-label">Mapped Value</label>
<input :id="`field-valuemap-mapped-${index}`" class="form-control" placeholder="Value to set" :value="v" v-on:change="updateValueMap(k, k, $event.target.value)" />
<label :for="`field-valuemap-mapped-${index}`" class="form-label" text-translate="true">Mapped Value</label>
<input :id="`field-valuemap-mapped-${index}`" class="form-control" placeholder="@StringLocalizer["Value to set"]" :value="v" v-on:change="updateValueMap(k, k, $event.target.value)" />
</div>
<button type="button" class="btn b-0 control remove" v-on:click="removeValueMap($event, k)">
<vc:icon symbol="remove" />
@ -202,7 +202,7 @@
</ol>
<h2>
@ViewData["Title"]
<a href="https://docs.btcpayserver.org/Forms" target="_blank" rel="noreferrer noopener" title="More information...">
<a href="https://docs.btcpayserver.org/Forms" target="_blank" rel="noreferrer noopener" title="@StringLocalizer["More information..."]">
<vc:icon symbol="info" />
</a>
</h2>
@ -230,10 +230,7 @@
<input asp-for="Public" type="checkbox" class="btcpay-toggle" />
<div>
<label asp-for="Public"></label>
<div class="form-text" text-translate="true">
Standalone mode, which can be used to generate invoices
independent of payment requests or apps.
</div>
<div class="form-text" text-translate="true">Standalone mode, which can be used to generate invoices independent of payment requests or apps.</div>
</div>
</div>
</div>
@ -272,7 +269,7 @@
<div class="col-xl-5 offcanvas-xl offcanvas-end" tabindex="-1" ref="editorOffcanvas">
<div class="offcanvas-header justify-content-between p-3">
<h5 class="offcanvas-title" text-translate="true">Edit Field</h5>
<button type="button" class="btn-close" aria-label="Close" v-on:click="hideOffcanvas">
<button type="button" class="btn-close" aria-label="@StringLocalizer["Close"]" v-on:click="hideOffcanvas">
<vc:icon symbol="close" />
</button>
</div>

View File

@ -36,11 +36,7 @@
</div>
</div>
<div class="lead text-center">
<p class="mb-0" text-translate="true">
The combination of words below are called your recovery phrase.
The recovery phrase allows you to access and restore your wallet.
Write them down on a piece of paper in the exact order:
</p>
<p class="mb-0" text-translate="true">The combination of words below are called your recovery phrase. The recovery phrase allows you to access and restore your wallet. Write them down on a piece of paper in the exact order:</p>
</div>
<ol id="RecoveryPhrase" data-mnemonic="@Model.Mnemonic" class="d-inline-block my-5 mx-auto ps-4">
@foreach (var word in Model.Words)

View File

@ -13,7 +13,7 @@
<label for="test-payment-amount" class="control-label form-label">Fake a {{cryptoCode}} payment for testing</label>
<div class="d-flex gap-2 mb-2">
<div class="input-group">
<input id="test-payment-amount" name="Amount" type="number" :step="isSats ? '1' : '0.00000001'" min="0" class="form-control" placeholder="Amount" v-model="amount" :readonly="paying || paymentMethodId === 'BTC-LN'" />
<input id="test-payment-amount" name="Amount" type="number" :step="isSats ? '1' : '0.00000001'" min="0" class="form-control" placeholder="@StringLocalizer["Amount"]" v-model="amount" :readonly="paying || paymentMethodId === 'BTC-LN'" />
<div id="test-payment-crypto-code" class="input-group-addon input-group-text" v-text="cryptoCode"></div>
</div>
<button class="btn btn-secondary flex-shrink-0 px-3 w-100px" type="submit" :disabled="paying" id="FakePayment">Pay</button>

View File

@ -9,7 +9,7 @@
<div class="form-group mb-1">
<label for="test-payment-amount" class="control-label">{{$t("Fake a @Model.PaymentMethodCurrency payment for testing")}}</label>
<div class="input-group">
<input id="test-payment-amount" name="Amount" type="number" step="0.00000001" min="0" class="form-control" placeholder="Amount" value="@Model.Due" />
<input id="test-payment-amount" name="Amount" type="number" step="0.00000001" min="0" class="form-control" placeholder="@StringLocalizer["Amount"]" value="@Model.Due" />
<div id="test-payment-crypto-code" class="input-group-addon">@Model.PaymentMethodCurrency</div>
</div>
</div>

View File

@ -52,7 +52,7 @@
</button>
</nav>
<section id="payment" v-if="isActive">
<div v-if="srvModel.itemDesc && srvModel.itemDesc !== srvModel.storeName" v-text="srvModel.itemDesc" class="fw-semibold text-center text-muted mb-3"></div>
<div v-if="srvModel.itemDesc && srvModel.itemDesc !== srvModel.storeName" v-text="srvModel.itemDesc" class="fw-semibold text-center text-break text-muted mb-3"></div>
<div class="d-flex justify-content-center mt-1 text-center">
@if (Model.IsUnsetTopUp)
{

View File

@ -3,7 +3,7 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.TagHelpers
@{
ViewData.SetActivePage(InvoiceNavPages.Create, "Create Invoice");
ViewData.SetActivePage(InvoiceNavPages.Create, StringLocalizer["Create Invoice"]);
}
@section PageFootContent {

View File

@ -155,7 +155,7 @@
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="RefundTitle">Issue Refund</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
</div>
@ -194,17 +194,17 @@
}
else
{
<button class="btn btn-secondary text-nowrap" data-bs-toggle="tooltip" title="You can only refund an invoice that has been settled. Please wait for the transaction to confirm on the blockchain before attempting to refund it." disabled>Issue refund</button>
<button class="btn btn-secondary text-nowrap" data-bs-toggle="tooltip" title="@StringLocalizer["You can only refund an invoice that has been settled. Please wait for the transaction to confirm on the blockchain before attempting to refund it."]" disabled>Issue refund</button>
}
<form asp-action="ToggleArchive" asp-route-invoiceId="@Model.Id" method="post">
<button type="submit" class="btn btn-secondary" id="btn-archive-toggle">
@if (Model.Archived)
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Unarchive this invoice">Unarchive</span>
<span class="text-nowrap" data-bs-toggle="tooltip" title="@StringLocalizer["Unarchive this invoice"]">Unarchive</span>
}
else
{
<span class="text-nowrap" data-bs-toggle="tooltip" title="Archive this invoice so that it does not appear in the invoice list by default">Archive</span>
<span class="text-nowrap" data-bs-toggle="tooltip" title="@StringLocalizer["Archive this invoice so that it does not appear in the invoice list by default"]">Archive</span>
}
</button>
</form>

View File

@ -1,47 +0,0 @@
@inject UserManager<ApplicationUser> _userManager
@* This is a temporary infobox to inform users about the state changes in 1.4.0. It should be removed eventually. *@
@if ((await _userManager.GetUserAsync(User)).GetBlob()?.ShowInvoiceStatusChangeHint is true)
{
<div class="alert alert-light alert-dismissible fade show mb-5" role="alert">
<form method="post" asp-controller="UIManage" asp-action="DisableShowInvoiceStatusChangeHint" id="invoicestatuschangeform">
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
<vc:icon symbol="close" />
</button>
<h5 class="alert-heading">Updated in v1.4.0</h5>
<p class="mb-2" text-translate="true">Invoice states have been updated to match the Greenfield API:</p>
<div class="row">
<div class="col-12 col-md-6">
<ul class="list-unstyled mb-md-0">
<li>
<span text-translate="true" class="badge badge-processing">Paid</span>
<span text-translate="true" class="mx-1">is now shown as</span>
<span text-translate="true" class="badge badge-processing">Processing</span>
</li>
<li class="mt-2">
<span text-translate="true" class="badge badge-settled">Completed</span>
<span text-translate="true" class="mx-1">is now shown as</span>
<span text-translate="true" class="badge badge-settled">Settled</span>
</li>
<li class="mt-2">
<span text-translate="true" class="badge badge-settled">Confirmed</span>
<span text-translate="true" class="mx-1">is now shown as</span>
<span text-translate="true" class="badge badge-settled">Settled</span>
</li>
</ul>
</div>
<div class="col-12 col-md-6 d-flex justify-content-md-end align-items-md-end">
<button name="command" type="submit" value="save" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="alert" text-translate="true">Don't Show Again</button>
</div>
</div>
</form>
</div>
<script>
document.getElementById("invoicestatuschangeform").addEventListener("submit", event => {
event.preventDefault();
const xhttp = new XMLHttpRequest();
xhttp.open('POST', event.target.getAttribute('action'), true);
xhttp.send(new FormData(event.target));
});
</script>
}

View File

@ -5,7 +5,7 @@
@inject DisplayFormatter DisplayFormatter
@model InvoicesModel
@{
ViewData.SetActivePage(InvoiceNavPages.Index, "Invoices");
ViewData.SetActivePage(InvoiceNavPages.Index, StringLocalizer["Invoices"]);
var statusFilterCount = CountArrayFilter("status") + CountArrayFilter("exceptionstatus") + (HasBooleanFilter("includearchived") ? 1 : 0) + (HasBooleanFilter("unusual") ? 1 : 0);
var hasDateFilter = HasArrayFilter("startdate") || HasArrayFilter("enddate");
var appFilterCount = Model.Apps.Count(app => HasArrayFilter("appid", app.Id));
@ -123,24 +123,20 @@
<div class="flex-fill">
<p text-translate="true" class="mb-2">Invoices are documents issued by the seller to a buyer to collect payment.</p>
<p text-translate="true" class="mb-3">An invoice must be paid within a defined time interval at a fixed exchange rate to protect the issuer from price fluctuations.</p>
<p class="mb-3">
You can also apply filters to your search by searching for <code>filtername:value</code>.
Be sure to split your search parameters with comma. Supported filters are:
</p>
<p class="mb-3" html-translate="true">You can also apply filters to your search by searching for <code>filtername:value</code>. Be sure to split your search parameters with comma. Supported filters are:</p>
<ul>
<li><code>orderid:id</code> for filtering a specific order</li>
<li><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
<li html-translate="true"><code>orderid:id</code> for filtering a specific order</li>
<li html-translate="true"><code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps</li>
</ul>
<a href="https://docs.btcpayserver.org/Invoices/" target="_blank" rel="noreferrer noopener" text-translate="true">Learn More</a>
</div>
<button type="button" class="btn-close ms-auto" data-bs-toggle="collapse" data-bs-target="#descriptor" aria-expanded="false" aria-label="Close">
<button type="button" class="btn-close ms-auto" data-bs-toggle="collapse" data-bs-target="#descriptor" aria-expanded="false" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
</div>
</div>
<partial name="_StatusMessage" />
<partial name="InvoiceStatusChangePartial" />
@* Custom Range Modal *@
<div class="modal fade" id="customRangeModal" tabindex="-1" role="dialog" aria-labelledby="customRangeModalTitle" aria-hidden="true" data-bs-backdrop="static">
@ -148,7 +144,7 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="customRangeModalTitle" text-translate="true">Filter invoices by Custom Range</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="@StringLocalizer["Close"]">
<vc:icon symbol="close" />
</button>
</div>
@ -159,8 +155,8 @@
<div class="input-group">
<input id="dtpStartDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="Start Date" />
<button type="button" class="btn btn-secondary input-group-clear" title="Clear">
placeholder="@StringLocalizer["Start Date"]" />
<button type="button" class="btn btn-secondary input-group-clear" title="@StringLocalizer["Clear"]">
<vc:icon symbol="close" />
</button>
</div>
@ -172,8 +168,8 @@
<div class="input-group">
<input id="dtpEndDate" class="form-control flatdtpicker" type="datetime-local"
data-fdtp='{ "enableTime": true, "enableSeconds": true, "dateFormat": "Y-m-d H:i:S", "time_24hr": true, "defaultHour": 0 }'
placeholder="End Date" />
<button type="button" class="btn btn-secondary input-group-clear" title="Clear">
placeholder="@StringLocalizer["End Date"]" />
<button type="button" class="btn btn-secondary input-group-clear" title="@StringLocalizer["Clear"]">
<vc:icon symbol="close" />
</button>
</div>
@ -191,12 +187,12 @@
<input asp-for="Count" type="hidden" />
<input asp-for="TimezoneOffset" type="hidden" />
<input asp-for="SearchTerm" type="hidden" value="@Model.Search.WithoutSearchText()"/>
<input asp-for="SearchText" class="form-control" placeholder="Search…" />
<input asp-for="SearchText" class="form-control" placeholder="@StringLocalizer["Search…"]" />
<div class="dropdown">
<button id="StatusOptionsToggle" class="btn btn-secondary dropdown-toggle dropdown-toggle-custom-caret" type="button" data-bs-toggle="dropdown" aria-expanded="false">
@if (statusFilterCount > 0)
{
<span>@statusFilterCount Status</span>
<span>@StringLocalizer["{0} Status", statusFilterCount]</span>
}
else
{
@ -287,7 +283,7 @@
<th class="date-col">
<div class="d-flex align-items-center gap-1">
<span text-translate="true">Date</span>
<button type="button" class="btn btn-link p-0 switch-time-format only-for-js" title="Switch date format">
<button type="button" class="btn btn-link p-0 switch-time-format only-for-js" title="@StringLocalizer["Switch date format"]">
<vc:icon symbol="time" />
</button>
</div>

View File

@ -2,7 +2,7 @@
@using BTCPayServer.Abstractions.Models
@model UILNURLController.EditLightningAddressVM
@{
ViewData.SetActivePage("LightningAddress", nameof(StoreNavPages), "Lightning Address", Context.GetStoreData().Id);
ViewData.SetActivePage("LightningAddress", nameof(StoreNavPages), StringLocalizer["Lightning Address"], Context.GetStoreData().Id);
}
@section PageHeadContent {
@ -65,21 +65,21 @@
<div id="AdvancedSettings" class="collapse @(showAdvancedOptions ? "show" : "")">
<div class="row">
<div class="col-12 col-sm-auto">
<div class="form-group" title="The currency to generate the invoice in when generated through this lightning address ">
<div class="form-group" title="@StringLocalizer["The currency to generate the invoice in when generated through this lightning address"]">
<label asp-for="Add.CurrencyCode" class="form-label"></label>
<input asp-for="Add.CurrencyCode" class="form-control w-auto" currency-selection style="max-width:16ch;"/>
<span asp-validation-for="Add.CurrencyCode" class="text-danger"></span>
</div>
</div>
<div class="col-12 col-sm-auto">
<div class="form-group" title="Minimum amount of sats to allow to be sent to this ln address">
<div class="form-group" title="@StringLocalizer["Minimum amount of sats to allow to be sent to this ln address"]">
<label asp-for="Add.Min" class="form-label"></label>
<input asp-for="Add.Min" class="form-control" type="number" inputmode="numeric" min="1" style="max-width:16ch;"/>
<span asp-validation-for="Add.Min" class="text-danger"></span>
</div>
</div>
<div class="col-12 col-sm-auto">
<div class="form-group" title="Maximum amount of sats to allow to be sent to this ln address">
<div class="form-group" title="@StringLocalizer["Maximum amount of sats to allow to be sent to this ln address"]">
<label asp-for="Add.Max" class="form-label"></label>
<input asp-for="Add.Max" class="form-control" type="number" inputmode="numeric" min="1" max="@int.MaxValue" style="max-width:16ch;"/>
<span asp-validation-for="Add.Max" class="text-danger"></span>
@ -162,7 +162,7 @@
}
else
{
<p class="text-secondary">
<p class="text-secondary" text-translate="true">
There are no Lightning Addresses yet.
</p>
}

View File

@ -1,7 +1,7 @@
@using LNURL
@model Uri
@{
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, "Register your Lightning node for LNURL Auth");
ViewData.SetActivePage(ManageNavPages.TwoFactorAuthentication, StringLocalizer["Register your Lightning node for LNURL Auth"]);
var formats = new Dictionary<string, string>
{
{ "Bech32", LNURL.EncodeUri(Model, "login", true).ToString().ToUpperInvariant() },

View File

@ -8,9 +8,9 @@
<h2 class="mt-1 mb-4" text-translate="true">@ViewData["Title"]</h2>
@foreach (var item in Model)
{
<div class="alert alert-@(item.Result == PayResult.Ok ? "success" : "danger") mb-3" role="alert">
<div class="alert alert-@(item.Success is true ? "success" : "danger") mb-3" role="alert">
<h5 class="alert-heading">
@(item.Result == PayResult.Ok ? "Sent" : "Failed")
@(item.Success is true ? "Sent" : "Failed")
@if (!string.IsNullOrEmpty(item.Message))
{
<span>- @item.Message</span>

View File

@ -1,9 +1,13 @@
@namespace BTCPayServer.Client
@using BTCPayServer.Abstractions.Models
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.TagHelpers
@using Microsoft.AspNetCore.Html
@using Microsoft.AspNetCore.Mvc.TagHelpers
@inject Security.ContentSecurityPolicies Csp
@model BTCPayServer.Controllers.UIManageController.ApiKeysViewModel
@{
ViewData.SetActivePage(ManageNavPages.APIKeys, "API Keys");
ViewData.SetActivePage(ManageNavPages.APIKeys, StringLocalizer["API Keys"]);
Csp.UnsafeEval();
}
@ -17,9 +21,10 @@
<div class="row">
<div class="col-xl-10 col-xxl-constrain">
<p>
The <a asp-controller="UIHome" asp-action="SwaggerDocs" target="_blank">BTCPay Server Greenfield API</a> offers programmatic access to your instance. You can manage your BTCPay
Server (e.g. stores, invoices, users) as well as automate workflows and integrations (see <a href="https://docs.btcpayserver.org/Development/GreenFieldExample/" rel="noreferrer noopener">use case examples</a>).
For that you need the API keys, which can be generated here. Find more information in the <a href="@Url.Action("SwaggerDocs", "UIHome")#section/Authentication" target="_blank" rel="noreferrer noopener">API authentication docs</a>.
@ViewLocalizer["The {0} offers programmatic access to your instance. You can manage your BTCPay Server (e.g. stores, invoices, users) as well as automate workflows and integrations (see {1}). For that you need the API keys, which can be generated here. Find more information in the {2}.",
Html.ActionLink(StringLocalizer["Greenfield API"], "SwaggerDocs", "UIHome", new { }, new { target = "_blank", rel = "noreferrer noopener" }),
new HtmlString($"<a href=\"https://docs.btcpayserver.org/Development/GreenFieldExample/\" target=\"_blank\" rel=\"noreferrer noopener\">{StringLocalizer["use case examples"]}</a>"),
Html.ActionLink(StringLocalizer["API authentication docs"], "SwaggerDocs", "UIHome", null, null, "section/Authentication", new { }, new { target = "_blank", rel = "noreferrer noopener" })]
</p>
@if (Model.ApiKeyDatas.Any())

View File

@ -113,7 +113,7 @@
<select asp-for="PermissionValues[i].SpecificStores[index]" class="form-select w-auto flex-grow-0" asp-items="@(new SelectList(new[] {store}, nameof(StoreData.Id), nameof(StoreData.StoreName), store.Id))"></select>
}
<span asp-validation-for="PermissionValues[i].SpecificStores[index]" class="text-danger"></span>
<button type="submit" title="Remove Store Permission" name="command" value="@($"{Model.PermissionValues[i].Permission}:remove-store:{index}")" class="btn btn-danger">
<button type="submit" title="@StringLocalizer["Remove Store Permission"]" name="command" value="@($"{Model.PermissionValues[i].Permission}:remove-store:{index}")" class="btn btn-danger">
Remove
</button>
</div>

View File

@ -90,10 +90,7 @@
<h2 class="h5 fw-semibold mt-4" text-translate="true">Permissions</h2>
@if (!groupedPermissions.Any())
{
<p text-translate="true">
There are no associated permissions to the API key being requested by the application.
The application cannot do anything with your BTCPay Server account other than validating your account exists.
</p>
<p text-translate="true">There are no associated permissions to the API key being requested by the application. The application cannot do anything with your BTCPay Server account other than validating your account exists.</p>
}
else
{

Some files were not shown because too many files have changed in this diff Show More