mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +01:00
Merge remote-tracking branch 'origin/master' into mobile-working-branch
This commit is contained in:
commit
ce9d2dc3d2
93 changed files with 1754 additions and 194 deletions
|
@ -9,6 +9,7 @@ namespace BTCPayServer.Configuration
|
|||
public string TempStorageDir { get; set; }
|
||||
public string StorageDir { get; set; }
|
||||
public string TempDir { get; set; }
|
||||
public string LangsDir { get; set; }
|
||||
|
||||
public string ToDatadirFullPath(string path)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20231219031609_translationsmigration")]
|
||||
public partial class translationsmigration : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
if (migrationBuilder.IsNpgsql())
|
||||
{
|
||||
migrationBuilder.Sql("""
|
||||
CREATE TABLE lang_dictionaries (
|
||||
dict_id TEXT PRIMARY KEY,
|
||||
fallback TEXT DEFAULT NULL,
|
||||
source TEXT DEFAULT NULL,
|
||||
metadata JSONB DEFAULT NULL,
|
||||
FOREIGN KEY (fallback) REFERENCES lang_dictionaries(dict_id) ON UPDATE CASCADE ON DELETE SET NULL
|
||||
);
|
||||
INSERT INTO lang_dictionaries(dict_id, source) VALUES ('English', 'Default');
|
||||
|
||||
CREATE TABLE lang_translations (
|
||||
dict_id TEXT NOT NULL,
|
||||
sentence TEXT NOT NULL,
|
||||
translation TEXT NOT NULL,
|
||||
PRIMARY KEY (dict_id, sentence),
|
||||
FOREIGN KEY (dict_id) REFERENCES lang_dictionaries(dict_id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE VIEW translations AS
|
||||
WITH RECURSIVE translations_with_paths AS (
|
||||
SELECT d.dict_id, t.sentence, t.translation, ARRAY[d.dict_id] AS path FROM lang_translations t
|
||||
INNER JOIN lang_dictionaries d USING (dict_id)
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT d.dict_id, t.sentence, t.translation, d.dict_id || t.path FROM translations_with_paths t
|
||||
INNER JOIN lang_dictionaries d ON d.fallback=t.dict_id
|
||||
),
|
||||
ranked_translations AS (
|
||||
SELECT *,
|
||||
ROW_NUMBER() OVER (PARTITION BY dict_id, sentence ORDER BY array_length(path, 1)) AS rn
|
||||
FROM translations_with_paths
|
||||
)
|
||||
SELECT dict_id, sentence, translation, path FROM ranked_translations WHERE rn=1;
|
||||
COMMENT ON VIEW translations IS 'Compute the translation for all sentences for all dictionaries, taking into account fallbacks';
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -47,7 +47,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToLogin();
|
||||
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.GoToProfile(ManageNavPages.APIKeys);
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
//not an admin, so this permission should not show
|
||||
Assert.DoesNotContain("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
|
||||
|
@ -56,34 +56,34 @@ namespace BTCPayServer.Tests
|
|||
s.GoToLogin();
|
||||
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.GoToProfile(ManageNavPages.APIKeys);
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("btcpay.server.canmodifyserversettings", s.Driver.PageSource);
|
||||
|
||||
//server management should show now
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.user.canviewprofile"), true);
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
s.ClickPagePrimary();
|
||||
var superApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
|
||||
//this api key has access to everything
|
||||
await TestApiAgainstAccessToken(superApiKey, tester, user, Policies.CanModifyServerSettings, Policies.CanModifyStoreSettings, Policies.CanViewProfile);
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.server.canmodifyserversettings"), true);
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
s.ClickPagePrimary();
|
||||
var serverOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
await TestApiAgainstAccessToken(serverOnlyApiKey, tester, user,
|
||||
Policies.CanModifyServerSettings);
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.SetCheckbox(By.Id("btcpay.store.canmodifystoresettings"), true);
|
||||
s.Driver.FindElement(By.Id("Generate")).Click();
|
||||
s.ClickPagePrimary();
|
||||
var allStoreOnlyApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
await TestApiAgainstAccessToken(allStoreOnlyApiKey, tester, user,
|
||||
Policies.CanModifyStoreSettings);
|
||||
|
||||
s.Driver.FindElement(By.Id("AddApiKey")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.CssSelector("button[value='btcpay.store.canmodifystoresettings:change-store-mode']")).Click();
|
||||
//there should be a store already by default in the dropdown
|
||||
var getPermissionValueIndex =
|
||||
|
@ -94,13 +94,13 @@ namespace BTCPayServer.Tests
|
|||
var option = dropdown.FindElement(By.TagName("option"));
|
||||
var storeId = option.GetAttribute("value");
|
||||
option.Click();
|
||||
s.Driver.WaitForAndClick(By.Id("Generate"));
|
||||
s.ClickPagePrimary();
|
||||
var selectiveStoreApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
await TestApiAgainstAccessToken(selectiveStoreApiKey, tester, user,
|
||||
Permission.Create(Policies.CanModifyStoreSettings, storeId).ToString());
|
||||
|
||||
s.Driver.WaitForAndClick(By.Id("AddApiKey"));
|
||||
s.Driver.WaitForAndClick(By.Id("Generate"));
|
||||
s.ClickPagePrimary(); // New API key
|
||||
s.ClickPagePrimary(); // Generate
|
||||
var noPermissionsApiKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
await TestApiAgainstAccessToken(noPermissionsApiKey, tester, user);
|
||||
await Assert.ThrowsAnyAsync<HttpRequestException>(async () =>
|
||||
|
@ -223,12 +223,12 @@ namespace BTCPayServer.Tests
|
|||
s.GoToLogin();
|
||||
s.LogIn(user.RegisterDetails.Email, user.RegisterDetails.Password);
|
||||
s.GoToUrl("/account/apikeys");
|
||||
s.Driver.WaitForAndClick(By.Id("AddApiKey"));
|
||||
s.ClickPagePrimary();
|
||||
int checkedPermissionCount = s.Driver.FindElements(By.ClassName("form-check-input")).Count;
|
||||
s.Driver.ExecuteJavaScript("document.querySelectorAll('#Permissions .form-check-input').forEach(i => i.click())");
|
||||
|
||||
TestLogs.LogInformation("Generating API key");
|
||||
s.Driver.WaitForAndClick(By.Id("Generate"));
|
||||
s.ClickPagePrimary();
|
||||
var allAPIKey = s.FindAlertMessage().FindElement(By.TagName("code")).Text;
|
||||
|
||||
TestLogs.LogInformation($"Checking API key permissions: {allAPIKey}");
|
||||
|
|
|
@ -21,10 +21,12 @@
|
|||
|
||||
<ItemGroup>
|
||||
<None Remove="TestData\OldInvoices.csv" />
|
||||
<None Remove="TestData\Langs\Cypherpunk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="TestData\OldInvoices.csv" />
|
||||
<Content Include="TestData\Langs\Cypherpunk" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -39,8 +39,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
public class BTCPayServerTester : IDisposable
|
||||
{
|
||||
private readonly string _Directory;
|
||||
|
||||
internal readonly string _Directory;
|
||||
public ILoggerProvider LoggerProvider { get; }
|
||||
|
||||
ILog TestLogs;
|
||||
|
@ -95,6 +94,13 @@ namespace BTCPayServer.Tests
|
|||
get; set;
|
||||
}
|
||||
|
||||
public async Task RestartStartupTask<T>()
|
||||
{
|
||||
var startupTask = GetService<IServiceProvider>().GetServices<Abstractions.Contracts.IStartupTask>()
|
||||
.Single(task => task is T);
|
||||
await startupTask.ExecuteAsync();
|
||||
}
|
||||
|
||||
public bool MockRates { get; set; } = true;
|
||||
public string SocksEndpoint { get; set; }
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ namespace BTCPayServer.Tests
|
|||
var supportUrl = "https://support.satoshisteaks.com/{InvoiceId}/";
|
||||
s.GoToStore();
|
||||
s.Driver.FindElement(By.Id("StoreWebsite")).SendKeys(storeUrl);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
|
@ -46,7 +46,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.WaitForAndClick(By.Id("Presets_InStore"));
|
||||
Assert.True(s.Driver.SetCheckbox(By.Id("ShowPayInWalletButton"), true));
|
||||
s.Driver.FindElement(By.Id("SupportUrl")).SendKeys(supportUrl);
|
||||
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
// Top up/zero amount invoices
|
||||
|
@ -105,7 +105,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToHome();
|
||||
s.GoToLightningSettings();
|
||||
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
|
@ -251,7 +251,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), true);
|
||||
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), false);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
invoiceId = s.CreateInvoice();
|
||||
|
@ -285,7 +285,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToHome();
|
||||
s.GoToStore(StoreNavPages.CheckoutAppearance);
|
||||
s.Driver.SetCheckbox(By.Id("LightningAmountInSatoshi"), true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
Assert.Contains("sats", s.Driver.FindElement(By.Id("AmountDue")).Text);
|
||||
|
@ -368,7 +368,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Equal("5", displayExpirationTimer.GetAttribute("value"));
|
||||
displayExpirationTimer.Clear();
|
||||
displayExpirationTimer.SendKeys("10");
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.GoToInvoiceCheckout(invoiceId);
|
||||
|
@ -392,8 +392,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToHome();
|
||||
s.GoToLightningSettings();
|
||||
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), false);
|
||||
s.Driver.ScrollTo(By.Id("save"));
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("BTC Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
// Test:
|
||||
|
|
|
@ -1,8 +1,19 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Hosting;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Tests.Logging;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OpenQA.Selenium;
|
||||
using OpenQA.Selenium.Support.Extensions;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static BTCPayServer.Services.LocalizerService;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
|
@ -14,6 +25,155 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Selenium", "Selenium")]
|
||||
public async Task CanTranslateLoginPage()
|
||||
{
|
||||
using var tester = CreateSeleniumTester(newDb: true);
|
||||
tester.Server.ActivateLangs();
|
||||
await tester.StartAsync();
|
||||
await tester.Server.PayTester.RestartStartupTask<LoadTranslationsStartupTask>();
|
||||
|
||||
// Check if the Cypherpunk translation has been loaded from the file
|
||||
tester.RegisterNewUser(true);
|
||||
tester.CreateNewStore();
|
||||
tester.GoToServer(Views.Server.ServerNavPages.Translations);
|
||||
tester.Driver.FindElement(By.Id("Select-Cypherpunk")).Click();
|
||||
tester.Logout();
|
||||
Assert.Contains("Cyphercode", tester.Driver.PageSource);
|
||||
Assert.Contains("Yo at BTCPay Server", tester.Driver.PageSource);
|
||||
|
||||
// Create English (Custom)
|
||||
tester.LogIn();
|
||||
tester.GoToServer(Views.Server.ServerNavPages.Translations);
|
||||
tester.ClickPagePrimary();
|
||||
tester.Driver.FindElement(By.Name("Name")).SendKeys("English (Custom)");
|
||||
tester.ClickPagePrimary();
|
||||
var translations = tester.Driver.FindElement(By.Name("Translations"));
|
||||
var text = translations.Text;
|
||||
text = text.Replace("Password => Password", "Password => Mot de passe");
|
||||
translations.Clear();
|
||||
translations.SendKeys("Password => Mot de passe");
|
||||
tester.ClickPagePrimary();
|
||||
|
||||
// Check English (Custom) can be selected
|
||||
tester.Driver.FindElement(By.Id("Select-English (Custom)")).Click();
|
||||
tester.Logout();
|
||||
Assert.Contains("Mot de passe", tester.Driver.PageSource);
|
||||
|
||||
// Check if we can remove English (Custom)
|
||||
tester.LogIn();
|
||||
tester.GoToServer(Views.Server.ServerNavPages.Translations);
|
||||
text = tester.Driver.PageSource;
|
||||
Assert.Contains("Select-Cypherpunk", text);
|
||||
Assert.DoesNotContain("Select-English (Custom)", text);
|
||||
// Cypherpunk is loaded from file, can't edit
|
||||
Assert.DoesNotContain("Delete-Cypherpunk", text);
|
||||
// English (Custom) is selected, can't edit
|
||||
Assert.DoesNotContain("Delete-English (Custom)", text);
|
||||
tester.Driver.FindElement(By.Id("Select-Cypherpunk")).Click();
|
||||
tester.Driver.FindElement(By.Id("Delete-English (Custom)")).Click();
|
||||
tester.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
|
||||
tester.Driver.FindElement(By.Id("ConfirmContinue")).Click();
|
||||
|
||||
text = tester.Driver.PageSource;
|
||||
Assert.DoesNotContain("Select-English (Custom)", text);
|
||||
Assert.Contains("English (Custom) deleted", text);
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanUpdateTranslationsInDatabase()
|
||||
{
|
||||
using var tester = CreateServerTester(newDb: true);
|
||||
await tester.StartAsync();
|
||||
var localizer = tester.PayTester.GetService<LocalizerService>();
|
||||
var factory = tester.PayTester.GetService<ApplicationDbContextFactory>();
|
||||
var db = factory.CreateContext().Database.GetDbConnection();
|
||||
|
||||
TestLogs.LogInformation("French fallback to english");
|
||||
await db.ExecuteAsync("INSERT INTO lang_dictionaries VALUES ('French', 'English', NULL)");
|
||||
|
||||
async Task SetDictionary(string dictId, (string Sentence, string Translation)[] translations)
|
||||
{
|
||||
var dict = await localizer.GetDictionary(dictId);
|
||||
var t = new Translations(translations.Select(t => KeyValuePair.Create(t.Sentence, t.Translation)));
|
||||
await localizer.Save(dict, t);
|
||||
}
|
||||
async Task AssertTranslations(string dictionary, (string Sentence, string Expected)[] expectations)
|
||||
{
|
||||
var all = await db.QueryAsync<(string sentence, string translation)>($"SELECT sentence, translation from translations WHERE dict_id='{dictionary}'");
|
||||
foreach (var expectation in expectations)
|
||||
{
|
||||
if (expectation.Expected is not null)
|
||||
Assert.Equal(expectation.Expected, all.Single(a => a.sentence == expectation.Sentence).translation);
|
||||
else
|
||||
Assert.DoesNotContain(all, a => a.sentence == expectation.Sentence);
|
||||
}
|
||||
}
|
||||
|
||||
await SetDictionary("English",
|
||||
[
|
||||
("Hello", "Hello"),
|
||||
("Goodbye", "Goodbye"),
|
||||
("Good afternoon", "Good afternoon")
|
||||
]);
|
||||
await SetDictionary("French",
|
||||
[
|
||||
("Hello", "Salut"),
|
||||
("Good afternoon", "Bonne aprem")
|
||||
]);
|
||||
|
||||
TestLogs.LogInformation("French should override Hello and Good afternoon, but not Goodbye");
|
||||
await AssertTranslations("French",
|
||||
[("Hello", "Salut"),
|
||||
("Good afternoon", "Bonne aprem"),
|
||||
("Goodbye", "Goodbye"),
|
||||
("lol", null)]);
|
||||
await AssertTranslations("English",
|
||||
[("Hello", "Hello"),
|
||||
("Good afternoon", "Good afternoon"),
|
||||
("Goodbye", "Goodbye"),
|
||||
("lol", null)]);
|
||||
|
||||
TestLogs.LogInformation("Can use fallback by setting null to a sentence");
|
||||
await SetDictionary("French",
|
||||
[
|
||||
("Good afternoon", "Bonne aprem"),
|
||||
("Goodbye", "Goodbye"),
|
||||
("Hello", null)
|
||||
]);
|
||||
await AssertTranslations("French",
|
||||
[("Hello", "Hello"),
|
||||
("Good afternoon", "Bonne aprem"),
|
||||
("Goodbye", "Goodbye"),
|
||||
("lol", null)]);
|
||||
|
||||
TestLogs.LogInformation("Can use fallback by setting same as fallback to a sentence");
|
||||
await SetDictionary("French",
|
||||
[
|
||||
("Good afternoon", "Good afternoon")
|
||||
]);
|
||||
await AssertTranslations("French",
|
||||
[("Hello", "Hello"),
|
||||
("Good afternoon", "Good afternoon"),
|
||||
("Goodbye", "Goodbye"),
|
||||
("lol", null)]);
|
||||
|
||||
await SetDictionary("English",
|
||||
[
|
||||
("Hello", null as string),
|
||||
("Good afternoon", "Good afternoon"),
|
||||
("Goodbye", "Goodbye")
|
||||
]);
|
||||
await AssertTranslations("French",
|
||||
[("Hello", null),
|
||||
("Good afternoon", "Good afternoon"),
|
||||
("Goodbye", "Goodbye"),
|
||||
("lol", null)]);
|
||||
await db.ExecuteAsync("DELETE FROM lang_dictionaries WHERE dict_id='English'");
|
||||
}
|
||||
|
||||
[Fact(Timeout = TestTimeout)]
|
||||
[Trait("Integration", "Integration")]
|
||||
public async Task CanAutoDetectLanguage()
|
||||
|
|
|
@ -325,14 +325,14 @@ retry:
|
|||
}
|
||||
}
|
||||
|
||||
Driver.FindElement(By.Id("save")).Click();
|
||||
ClickPagePrimary();
|
||||
Assert.Contains($"{cryptoCode} Lightning node updated.", FindAlertMessage().Text);
|
||||
|
||||
var enabled = Driver.FindElement(By.Id($"{cryptoCode}LightningEnabled"));
|
||||
if (enabled.Selected == false)
|
||||
{
|
||||
enabled.Click();
|
||||
Driver.FindElement(By.Id("save")).Click();
|
||||
ClickPagePrimary();
|
||||
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", FindAlertMessage().Text);
|
||||
}
|
||||
}
|
||||
|
@ -396,6 +396,10 @@ retry:
|
|||
Driver.FindElement(By.Id("Nav-Logout")).Click();
|
||||
}
|
||||
|
||||
public void LogIn()
|
||||
{
|
||||
LogIn(CreatedUser, "123456");
|
||||
}
|
||||
public void LogIn(string user, string password = "123456")
|
||||
{
|
||||
Driver.FindElement(By.Id("Email")).SendKeys(user);
|
||||
|
@ -513,7 +517,7 @@ retry:
|
|||
{
|
||||
GoToInvoices(storeId);
|
||||
|
||||
Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||
ClickPagePrimary();
|
||||
if (amount is decimal v)
|
||||
Driver.FindElement(By.Id("Amount")).SendKeys(v.ToString(CultureInfo.InvariantCulture));
|
||||
var currencyEl = Driver.FindElement(By.Id("Currency"));
|
||||
|
@ -522,7 +526,7 @@ retry:
|
|||
Driver.FindElement(By.Id("BuyerEmail")).SendKeys(refundEmail);
|
||||
if (defaultPaymentMethod is not null)
|
||||
new SelectElement(Driver.FindElement(By.Name("DefaultPaymentMethod"))).SelectByValue(defaultPaymentMethod);
|
||||
Driver.FindElement(By.Id("Create")).Click();
|
||||
ClickPagePrimary();
|
||||
|
||||
var statusElement = FindAlertMessage(expectedSeverity);
|
||||
var inv = expectedSeverity == StatusMessageModel.StatusSeverity.Success ? statusElement.Text.Split(" ")[1] : null;
|
||||
|
@ -639,10 +643,22 @@ retry:
|
|||
name = $"{type}-{Guid.NewGuid().ToString()[..14]}";
|
||||
Driver.FindElement(By.Id($"StoreNav-Create{type}")).Click();
|
||||
Driver.FindElement(By.Name("AppName")).SendKeys(name);
|
||||
Driver.FindElement(By.Id("Create")).Click();
|
||||
ClickPagePrimary();
|
||||
Assert.Contains("App successfully created", FindAlertMessage().Text);
|
||||
var appId = Driver.Url.Split('/')[4];
|
||||
return (name, appId);
|
||||
}
|
||||
|
||||
public void ClickPagePrimary()
|
||||
{
|
||||
try
|
||||
{
|
||||
Driver.FindElement(By.Id("page-primary")).Click();
|
||||
}
|
||||
catch (NoSuchElementException)
|
||||
{
|
||||
Driver.WaitForAndClick(By.Id("page-primary"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,11 +84,11 @@ namespace BTCPayServer.Tests
|
|||
var appName = $"PoS-{Guid.NewGuid().ToString()[..21]}";
|
||||
s.Driver.FindElement(By.Id("StoreNav-CreatePointOfSale")).Click();
|
||||
s.Driver.FindElement(By.Id("AppName")).SendKeys(appName);
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App successfully created", s.FindAlertMessage().Text);
|
||||
|
||||
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
|
@ -110,12 +110,13 @@ namespace BTCPayServer.Tests
|
|||
|
||||
// Payment Request
|
||||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Thread.Sleep(10000);
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("700");
|
||||
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.Driver.TakeScreenshot().SaveAsFile("C:\\Users\\NicolasDorier\\Downloads\\chromedriver-win64\\1.png");
|
||||
s.ClickPagePrimary();
|
||||
|
||||
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
|
||||
var editUrl = s.Driver.Url;
|
||||
|
@ -127,7 +128,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.Contains("Enter your email", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Name("buyerEmail")).SendKeys("aa@aa.com");
|
||||
s.Driver.FindElement(By.CssSelector("input[type='submit']")).Click();
|
||||
s.ClickPagePrimary();
|
||||
invoiceId = s.Driver.Url.Split('/').Last();
|
||||
s.Driver.Close();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.First());
|
||||
|
@ -140,7 +141,7 @@ namespace BTCPayServer.Tests
|
|||
//Custom Forms
|
||||
s.GoToStore(StoreNavPages.Forms);
|
||||
Assert.Contains("There are no forms yet.", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("CreateForm")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 1");
|
||||
s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click();
|
||||
|
||||
|
@ -153,7 +154,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Name("FormConfig")).Clear();
|
||||
s.Driver.FindElement(By.Name("FormConfig"))
|
||||
.SendKeys(config.Replace("Enter your email", "CustomFormInputTest"));
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.Id("ViewForm")).Click();
|
||||
|
||||
var formurl = s.Driver.Url;
|
||||
|
@ -172,7 +173,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
|
||||
|
||||
Assert.DoesNotContain("Custom Form 1", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("CreateForm")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 2");
|
||||
s.Driver.FindElement(By.Id("ApplyEmailTemplate")).Click();
|
||||
|
||||
|
@ -184,7 +185,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Name("FormConfig")).Clear();
|
||||
s.Driver.FindElement(By.Name("FormConfig"))
|
||||
.SendKeys(config.Replace("Enter your email", "CustomFormInputTest2"));
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.Id("ViewForm")).Click();
|
||||
formurl = s.Driver.Url;
|
||||
result = await s.Server.PayTester.HttpClient.GetAsync(formurl);
|
||||
|
@ -198,12 +199,12 @@ namespace BTCPayServer.Tests
|
|||
|
||||
s.Driver.FindElement(By.Name("Name")).Clear();
|
||||
s.Driver.FindElement(By.Name("Name")).SendKeys("Custom Form 3");
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.GoToStore(StoreNavPages.Forms);
|
||||
Assert.Contains("Custom Form 3", s.Driver.PageSource);
|
||||
|
||||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Equal(4, new SelectElement(s.Driver.FindElement(By.Id("FormId"))).Options.Count);
|
||||
}
|
||||
|
||||
|
@ -296,7 +297,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToProfile();
|
||||
s.Driver.FindElement(By.Id("Email")).Clear();
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(u2.RegisterDetails.Email);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
Assert.Contains("The email address is already in use with an other account.",
|
||||
s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
|
||||
|
@ -305,7 +306,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("Email")).Clear();
|
||||
var changedEmail = Guid.NewGuid() + "@lol.com";
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(changedEmail);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.FindAlertMessage();
|
||||
|
||||
var manager = tester.PayTester.GetService<UserManager<ApplicationUser>>();
|
||||
|
@ -348,7 +349,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("OldPassword")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("NewPassword")).SendKeys(newPassword);
|
||||
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys(newPassword);
|
||||
s.Driver.FindElement(By.Id("UpdatePassword")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Logout();
|
||||
s.Driver.AssertNoError();
|
||||
|
||||
|
@ -367,11 +368,11 @@ namespace BTCPayServer.Tests
|
|||
s.RegisterNewUser(true);
|
||||
s.GoToHome();
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("CreateUser")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
var usr = RandomUtils.GetUInt256().ToString().Substring(64 - 20) + "@a.com";
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
var url = s.FindAlertMessage().FindElement(By.TagName("a")).Text;
|
||||
|
||||
s.Logout();
|
||||
|
@ -383,7 +384,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
s.Driver.FindElement(By.Id("Password")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("ConfirmPassword")).SendKeys("123456");
|
||||
s.Driver.FindElement(By.Id("SetPassword")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Account successfully created.", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("Email")).SendKeys(usr);
|
||||
|
@ -422,7 +423,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.True(s.Driver.FindElement(By.Id("EnableRegistration")).Selected);
|
||||
Assert.False(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
|
||||
s.Driver.FindElement(By.Id("RequiresUserApproval")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Policies updated successfully", s.FindAlertMessage().Text);
|
||||
Assert.True(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
|
||||
|
||||
|
@ -458,13 +459,13 @@ namespace BTCPayServer.Tests
|
|||
Assert.True(s.Driver.FindElement(By.Id("EnableRegistration")).Selected);
|
||||
Assert.True(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
|
||||
s.Driver.FindElement(By.Id("RequiresUserApproval")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Policies updated successfully", s.FindAlertMessage().Text);
|
||||
Assert.False(s.Driver.FindElement(By.Id("RequiresUserApproval")).Selected);
|
||||
|
||||
// Check user create view does not have approval checkbox
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
s.Driver.FindElement(By.Id("CreateUser")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.ElementDoesNotExist(By.Id("Approved"));
|
||||
|
||||
s.Logout();
|
||||
|
@ -521,7 +522,7 @@ namespace BTCPayServer.Tests
|
|||
// Approve user
|
||||
s.Driver.FindElement(By.CssSelector("#UsersList tr.user-overview-row:first-child .user-edit")).Click();
|
||||
s.Driver.FindElement(By.Id("Approved")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveUser")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("User successfully updated", s.FindAlertMessage().Text);
|
||||
// Check list again
|
||||
s.GoToServer(ServerNavPages.Users);
|
||||
|
@ -614,7 +615,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("Settings_Login")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_Password")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_From")).Clear();
|
||||
s.Driver.FindElement(By.Id("Save")).Submit();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
// Store Emails without server fallback
|
||||
s.GoToStore(StoreNavPages.Emails);
|
||||
|
@ -680,7 +681,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
|
||||
}
|
||||
|
||||
s.Driver.FindElement(By.Id("AddDynamicDNS")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.AssertNoError();
|
||||
// We will just cheat for test purposes by only querying the server
|
||||
s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/"));
|
||||
|
@ -692,7 +693,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.EndsWith("/server/services/dynamic-dns", s.Driver.Url);
|
||||
|
||||
// Try to do the same thing should fail (hostname already exists)
|
||||
s.Driver.FindElement(By.Id("AddDynamicDNS")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.AssertNoError();
|
||||
s.Driver.FindElement(By.Id("ServiceUrl")).SendKeys(s.Link("/"));
|
||||
s.Driver.FindElement(By.Id("Settings_Hostname")).SendKeys("pouet.hello.com");
|
||||
|
@ -721,7 +722,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToInvoices();
|
||||
|
||||
// Should give us an error message if we try to create an invoice before adding a wallet
|
||||
s.Driver.FindElement(By.Id("CreateNewInvoice")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("To create an invoice, you need to", s.Driver.PageSource);
|
||||
|
||||
s.AddDerivationScheme();
|
||||
|
@ -955,18 +956,18 @@ namespace BTCPayServer.Tests
|
|||
s.GoToStore();
|
||||
Assert.False(s.Driver.FindElement(By.Id("AnyoneCanCreateInvoice")).Selected);
|
||||
s.Driver.SetCheckbox(By.Id("AnyoneCanCreateInvoice"), true);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.FindAlertMessage();
|
||||
Assert.True(s.Driver.FindElement(By.Id("AnyoneCanCreateInvoice")).Selected);
|
||||
|
||||
// Store settings: Set and unset brand color
|
||||
s.GoToStore();
|
||||
s.Driver.FindElement(By.Id("BrandColor")).SendKeys("#f7931a");
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
Assert.Equal("#f7931a", s.Driver.FindElement(By.Id("BrandColor")).GetAttribute("value"));
|
||||
s.Driver.FindElement(By.Id("BrandColor")).Clear();
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Store successfully updated", s.FindAlertMessage().Text);
|
||||
Assert.Equal(string.Empty, s.Driver.FindElement(By.Id("BrandColor")).GetAttribute("value"));
|
||||
|
||||
|
@ -1014,10 +1015,10 @@ namespace BTCPayServer.Tests
|
|||
|
||||
s.GoToStore(StoreNavPages.Tokens);
|
||||
s.Driver.FindElement(By.Id("CreateNewToken")).Click();
|
||||
s.Driver.FindElement(By.Id("RequestPairing")).Click();
|
||||
s.ClickPagePrimary();
|
||||
var pairingCode = AssertUrlHasPairingCode(s);
|
||||
|
||||
s.Driver.FindElement(By.Id("ApprovePairing")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.FindAlertMessage();
|
||||
Assert.Contains(pairingCode, s.Driver.PageSource);
|
||||
|
||||
|
@ -1031,15 +1032,15 @@ namespace BTCPayServer.Tests
|
|||
|
||||
var code = await client.RequestClientAuthorizationAsync("hehe", NBitpayClient.Facade.Merchant);
|
||||
s.Driver.Navigate().GoToUrl(code.CreateLink(s.ServerUri));
|
||||
s.Driver.FindElement(By.Id("ApprovePairing")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
await client.CreateInvoiceAsync(
|
||||
new NBitpayClient.Invoice() { Price = 1.000000012m, Currency = "USD", FullNotifications = true },
|
||||
NBitpayClient.Facade.Merchant);
|
||||
|
||||
s.Driver.Navigate().GoToUrl(s.Link("/api-tokens"));
|
||||
s.Driver.FindElement(By.Id("RequestPairing")).Click();
|
||||
s.Driver.FindElement(By.Id("ApprovePairing")).Click();
|
||||
s.ClickPagePrimary(); // Request
|
||||
s.ClickPagePrimary(); // Approve
|
||||
AssertUrlHasPairingCode(s);
|
||||
}
|
||||
|
||||
|
@ -1157,15 +1158,15 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("Title")).SendKeys("Tea shop");
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Cart']")).Click();
|
||||
s.Driver.FindElement(By.CssSelector(".template-item:nth-of-type(1)")).Click();
|
||||
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
|
||||
s.Driver.FindElement(By.Id("BuyButtonText")).SendKeys("Take my money");
|
||||
s.Driver.FindElement(By.Id("EditorCategories-ts-control")).SendKeys("Drinks");
|
||||
s.Driver.ScrollTo(By.Id("CodeTabButton"));
|
||||
s.Driver.ScrollTo(By.Id("CodeTabButton"));
|
||||
s.Driver.FindElement(By.Id("CodeTabButton")).Click();
|
||||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
Assert.Contains("\"buyButtonText\": \"Take my money\"", template);
|
||||
Assert.Matches("\"categories\": \\[\n\\s+\"Drinks\"\n\\s+\\]", template);
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
|
@ -1198,7 +1199,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.ScrollTo(By.Id("RootAppId"));
|
||||
var select = new SelectElement(s.Driver.FindElement(By.Id("RootAppId")));
|
||||
select.SelectByText("Point of", true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.FindAlertMessage();
|
||||
// Make sure after login, we are not redirected to the PoS
|
||||
s.Logout();
|
||||
|
@ -1218,13 +1219,13 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.ScrollTo(By.Id("RootAppId"));
|
||||
select = new SelectElement(s.Driver.FindElement(By.Id("RootAppId")));
|
||||
select.SelectByText("None", true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.ScrollTo(By.Id("RootAppId"));
|
||||
s.Driver.FindElement(By.Id("AddDomainButton")).Click();
|
||||
s.Driver.FindElement(By.Id("DomainToAppMapping_0__Domain")).SendKeys(new Uri(s.Driver.Url, UriKind.Absolute).DnsSafeHost);
|
||||
select = new SelectElement(s.Driver.FindElement(By.Id("DomainToAppMapping_0__AppId")));
|
||||
select.SelectByText("Point of", true);
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Policies updated successfully", s.FindAlertMessage().Text);
|
||||
// Make sure after login, we are not redirected to the PoS
|
||||
s.Logout();
|
||||
|
@ -1276,13 +1277,13 @@ namespace BTCPayServer.Tests
|
|||
// test wrong dates
|
||||
s.Driver.ExecuteJavaScript("const now = new Date();document.getElementById('StartDate').value = now.toISOString();" +
|
||||
"const yst = new Date(now.setDate(now.getDate() -1));document.getElementById('EndDate').value = yst.toISOString()");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("End date cannot be before start date", s.Driver.PageSource);
|
||||
Assert.DoesNotContain("App updated", s.Driver.PageSource);
|
||||
|
||||
// unset end date
|
||||
s.Driver.ExecuteJavaScript("document.getElementById('EndDate').value = ''");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
var editUrl = s.Driver.Url;
|
||||
|
||||
|
@ -1337,7 +1338,7 @@ namespace BTCPayServer.Tests
|
|||
// Crowdfund with form
|
||||
s.GoToUrl(editUrl);
|
||||
new SelectElement(s.Driver.FindElement(By.Id("FormId"))).SelectByValue("Email");
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
|
@ -1371,7 +1372,7 @@ namespace BTCPayServer.Tests
|
|||
var template = s.Driver.FindElement(By.Id("TemplateConfig")).GetAttribute("value");
|
||||
Assert.Contains("\"title\": \"Perk 1\"", template);
|
||||
Assert.Contains("\"id\": \"Perk-1\"", template);
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
|
@ -1402,12 +1403,12 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||
|
||||
// Should give us an error message if we try to create a payment request before adding a wallet
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("To create a payment request, you need to", s.Driver.PageSource);
|
||||
|
||||
s.AddDerivationScheme();
|
||||
s.Driver.FindElement(By.Id("StoreNav-PaymentRequests")).Click();
|
||||
s.Driver.FindElement(By.Id("CreatePaymentRequest")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.Id("Title")).SendKeys("Pay123");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys(".01");
|
||||
|
@ -1417,7 +1418,7 @@ namespace BTCPayServer.Tests
|
|||
currencyInput.Clear();
|
||||
currencyInput.SendKeys("BTC");
|
||||
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
|
||||
var editUrl = s.Driver.Url;
|
||||
|
||||
|
@ -1430,7 +1431,7 @@ namespace BTCPayServer.Tests
|
|||
|
||||
// expire
|
||||
s.Driver.ExecuteJavaScript("document.getElementById('ExpiryDate').value = '2021-01-21T21:00:00.000Z'");
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
|
||||
|
||||
s.GoToUrl(viewUrl);
|
||||
|
@ -1439,7 +1440,7 @@ namespace BTCPayServer.Tests
|
|||
// unexpire
|
||||
s.GoToUrl(editUrl);
|
||||
s.Driver.FindElement(By.Id("ClearExpiryDate")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.XPath("//a[starts-with(@id, 'Edit-')]")).Click();
|
||||
|
||||
// amount and currency should be editable, because no invoice exists
|
||||
|
@ -1606,12 +1607,12 @@ namespace BTCPayServer.Tests
|
|||
TestLogs.LogInformation("Let's create two webhooks");
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
s.Driver.FindElement(By.Id("CreateWebhook")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.Name("PayloadUrl")).SendKeys($"http://127.0.0.1/callback{i}");
|
||||
new SelectElement(s.Driver.FindElement(By.Id("Everything"))).SelectByValue("false");
|
||||
s.Driver.FindElement(By.Id("InvoiceCreated")).Click();
|
||||
s.Driver.FindElement(By.Id("InvoiceProcessing")).Click();
|
||||
s.Driver.FindElement(By.Name("add")).Click();
|
||||
s.ClickPagePrimary();
|
||||
}
|
||||
|
||||
TestLogs.LogInformation("Let's delete one of them");
|
||||
|
@ -2006,7 +2007,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("ConnectionString")).SendKeys("type=lnd-rest;server=https://doesnotwork:8080/");
|
||||
s.Driver.FindElement(By.Id("test")).Click();
|
||||
Assert.Contains("Error", s.FindAlertMessage(StatusMessageModel.StatusSeverity.Error).Text);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("BTC Lightning node updated.", s.FindAlertMessage().Text);
|
||||
|
||||
// Check offline state is communicated in nav item
|
||||
|
@ -2066,7 +2067,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
@ -2082,7 +2083,7 @@ namespace BTCPayServer.Tests
|
|||
name.SendKeys("PP1 Edited");
|
||||
var description = s.Driver.FindElement(By.ClassName("card-block"));
|
||||
description.SendKeys("Description Edit");
|
||||
s.Driver.FindElement(By.Id("SaveButton")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
@ -2111,7 +2112,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP1");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("99.0");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
@ -2125,7 +2126,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("Name")).SendKeys("PP2");
|
||||
s.Driver.FindElement(By.Id("Amount")).Clear();
|
||||
s.Driver.FindElement(By.Id("Amount")).SendKeys("100.0");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
// This should select the first View, ie, the last one PP2
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
|
@ -2230,7 +2231,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("Amount")).SendKeys("0.001");
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
@ -2285,7 +2286,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("Amount")).SendKeys(payoutAmount.ToString());
|
||||
s.Driver.FindElement(By.Id("Currency")).Clear();
|
||||
s.Driver.FindElement(By.Id("Currency")).SendKeys("BTC");
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
|
||||
|
@ -2560,7 +2561,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
|
||||
s.CreateApp("PointOfSale");
|
||||
s.Driver.FindElement(By.CssSelector("label[for='DefaultView_Print']")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.Driver.FindElement(By.Id("ViewApp")).Click();
|
||||
|
@ -2610,7 +2611,7 @@ namespace BTCPayServer.Tests
|
|||
Assert.False(s.Driver.FindElement(By.Id("ShowDiscount")).Selected);
|
||||
Assert.False(s.Driver.FindElement(By.Id("ShowItems")).Selected);
|
||||
s.Driver.FindElement(By.Id("ShowDiscount")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
// View
|
||||
|
@ -2726,7 +2727,7 @@ namespace BTCPayServer.Tests
|
|||
// Once more with items
|
||||
s.GoToUrl(editUrl);
|
||||
s.Driver.FindElement(By.Id("ShowItems")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
s.GoToUrl(keypadUrl);
|
||||
|
@ -2842,7 +2843,7 @@ namespace BTCPayServer.Tests
|
|||
s.Driver.FindElement(By.Id("CustomTipPercentages")).SendKeys("10,21");
|
||||
Assert.False(s.Driver.FindElement(By.Id("ShowDiscount")).Selected);
|
||||
s.Driver.FindElement(By.Id("ShowDiscount")).Click();
|
||||
s.Driver.FindElement(By.Id("SaveSettings")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("App updated", s.FindAlertMessage().Text);
|
||||
|
||||
// View
|
||||
|
@ -3008,7 +3009,7 @@ namespace BTCPayServer.Tests
|
|||
// LNURL is true by default
|
||||
Assert.True(s.Driver.FindElement(By.Id("LNURLEnabled")).Selected);
|
||||
s.Driver.SetCheckbox(By.Name("LUD12Enabled"), true);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
// Topup Invoice test
|
||||
var i = s.CreateInvoice(storeId, null, cryptoCode);
|
||||
|
@ -3094,7 +3095,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToHome();
|
||||
s.GoToLightningSettings();
|
||||
s.Driver.SetCheckbox(By.Id("LNURLBech32Mode"), false);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
|
||||
// Ensure the toggles are set correctly
|
||||
|
@ -3112,7 +3113,7 @@ namespace BTCPayServer.Tests
|
|||
s.AddLightningNode(LightningConnectionType.LndREST, false);
|
||||
s.GoToLightningSettings();
|
||||
s.Driver.SetCheckbox(By.Id("LNURLEnabled"), true);
|
||||
s.Driver.FindElement(By.Id("save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
var invForPP = s.CreateInvoice(null, cryptoCode);
|
||||
s.GoToInvoiceCheckout(invForPP);
|
||||
|
@ -3132,7 +3133,7 @@ namespace BTCPayServer.Tests
|
|||
currencyInput.Clear();
|
||||
currencyInput.SendKeys("BTC");
|
||||
|
||||
s.Driver.FindElement(By.Id("Create")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.LinkText("View")).Click();
|
||||
s.Driver.SwitchTo().Window(s.Driver.WindowHandles.Last());
|
||||
var pullPaymentId = s.Driver.Url.Split('/').Last();
|
||||
|
@ -3338,7 +3339,7 @@ namespace BTCPayServer.Tests
|
|||
s.GoToHome();
|
||||
s.GoToProfile(ManageNavPages.LoginCodes);
|
||||
var code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||
s.Driver.FindElement(By.Id("regeneratecode")).Click();
|
||||
s.ClickPagePrimary();
|
||||
Assert.NotEqual(code, s.Driver.FindElement(By.Id("logincode")).GetAttribute("value"));
|
||||
|
||||
code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||
|
@ -3359,7 +3360,7 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
int maxAttempts = 5;
|
||||
retry:
|
||||
s.Driver.WaitForAndClick(By.Id("save"));
|
||||
s.ClickPagePrimary();
|
||||
try
|
||||
{
|
||||
Assert.Contains($"{cryptoCode} Lightning settings successfully updated", s.FindAlertMessage().Text);
|
||||
|
@ -3535,12 +3536,12 @@ retry:
|
|||
s.FindAlertMessage();
|
||||
|
||||
s.GoToStore(StoreNavPages.Roles);
|
||||
s.Driver.FindElement(By.Id("CreateRole")).Click();
|
||||
s.ClickPagePrimary();
|
||||
|
||||
Assert.Contains("Create role", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.Id("Role")).SendKeys("store role");
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.FindAlertMessage();
|
||||
|
||||
existingStoreRoles = s.Driver.FindElement(By.CssSelector("table")).FindElements(By.CssSelector("tr"));
|
||||
|
@ -3571,12 +3572,12 @@ retry:
|
|||
Assert.DoesNotContain(options, element => element.Text.Equals("store role", StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
s.GoToStore(StoreNavPages.Roles);
|
||||
s.Driver.FindElement(By.Id("CreateRole")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.Driver.FindElement(By.Id("Role")).SendKeys("Malice");
|
||||
|
||||
s.Driver.ExecuteJavaScript($"document.getElementById('Policies')['{Policies.CanModifyServerSettings}']=new Option('{Policies.CanModifyServerSettings}', '{Policies.CanModifyServerSettings}', true,true);");
|
||||
|
||||
s.Driver.FindElement(By.Id("Save")).Click();
|
||||
s.ClickPagePrimary();
|
||||
s.FindAlertMessage();
|
||||
Assert.Contains("Malice",s.Driver.PageSource);
|
||||
Assert.DoesNotContain(Policies.CanModifyServerSettings,s.Driver.PageSource);
|
||||
|
@ -3858,11 +3859,11 @@ retry:
|
|||
s.Driver.FindElement(By.Id("Settings_Password")).SendKeys("mypassword");
|
||||
s.Driver.FindElement(By.Id("Settings_From")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_From")).SendKeys("Firstname Lastname <email@example.com>");
|
||||
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Configured", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("Settings_Login")).Clear();
|
||||
s.Driver.FindElement(By.Id("Settings_Login")).SendKeys("test_fix@gmail.com");
|
||||
s.Driver.FindElement(By.Id("Save")).SendKeys(Keys.Enter);
|
||||
s.ClickPagePrimary();
|
||||
Assert.Contains("Configured", s.Driver.PageSource);
|
||||
Assert.Contains("test_fix", s.Driver.PageSource);
|
||||
s.Driver.FindElement(By.Id("ResetPassword")).SendKeys(Keys.Enter);
|
||||
|
|
|
@ -18,6 +18,8 @@ using NBitcoin;
|
|||
using NBitcoin.RPC;
|
||||
using NBitpayClient;
|
||||
using NBXplorer;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
|
@ -72,6 +74,17 @@ namespace BTCPayServer.Tests
|
|||
PayTester.SSHConnection = GetEnvironment("TESTS_SSHCONNECTION", "root@127.0.0.1:21622");
|
||||
PayTester.SocksEndpoint = GetEnvironment("TESTS_SOCKSENDPOINT", "localhost:9050");
|
||||
}
|
||||
|
||||
public void ActivateLangs()
|
||||
{
|
||||
TestLogs.LogInformation("Activating Langs...");
|
||||
var dir = TestUtils.GetTestDataFullPath("Langs");
|
||||
var langdir = Path.Combine(PayTester._Directory, "Langs");
|
||||
Directory.CreateDirectory(langdir);
|
||||
foreach (var file in Directory.GetFiles(dir))
|
||||
File.Copy(file, Path.Combine(langdir, Path.GetFileName(file)));
|
||||
}
|
||||
|
||||
#if ALTCOINS
|
||||
public void ActivateLTC()
|
||||
{
|
||||
|
|
3
BTCPayServer.Tests/TestData/Langs/Cypherpunk
Normal file
3
BTCPayServer.Tests/TestData/Langs/Cypherpunk
Normal file
|
@ -0,0 +1,3 @@
|
|||
Password => Cyphercode
|
||||
Email address => Cypher ID
|
||||
Welcome to {0} => Yo at {0}
|
|
@ -2878,9 +2878,7 @@ namespace BTCPayServer.Tests
|
|||
{
|
||||
var settings = tester.PayTester.GetService<SettingsRepository>();
|
||||
await settings.UpdateSetting<MigrationSettings>(new MigrationSettings());
|
||||
var migrationStartupTask = tester.PayTester.GetService<IServiceProvider>().GetServices<IStartupTask>()
|
||||
.Single(task => task is MigrationStartupTask);
|
||||
await migrationStartupTask.ExecuteAsync();
|
||||
await tester.PayTester.RestartStartupTask<MigrationStartupTask>();
|
||||
}
|
||||
|
||||
[Fact(Timeout = LongRunningTestTimeout)]
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.Auth.AccessControlPolicy;
|
||||
|
@ -13,6 +15,10 @@ using BTCPayServer.Client;
|
|||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using ExchangeSharp;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
@ -21,18 +27,17 @@ using OpenQA.Selenium.Chrome;
|
|||
using OpenQA.Selenium.Support.UI;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using static BTCPayServer.Tests.TransifexClient;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// This class hold easy to run utilities for dev time
|
||||
/// </summary>
|
||||
public class UtilitiesTests
|
||||
public class UtilitiesTests : UnitTestBase
|
||||
{
|
||||
public ITestOutputHelper Logs { get; }
|
||||
|
||||
public UtilitiesTests(ITestOutputHelper logs)
|
||||
public UtilitiesTests(ITestOutputHelper logs) : base(logs)
|
||||
{
|
||||
Logs = logs;
|
||||
}
|
||||
|
@ -262,6 +267,120 @@ retry:
|
|||
Thread.Sleep(200);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This utilities crawl through the cs files in search for
|
||||
/// Display attributes, then update Translations.Default to list them
|
||||
/// </summary>
|
||||
[Trait("Utilities", "Utilities")]
|
||||
[Fact]
|
||||
public async Task UpdateDefaultTranslations()
|
||||
{
|
||||
var soldir = TestUtils.TryGetSolutionDirectoryInfo();
|
||||
List<string> defaultTranslatedKeys = new List<string>();
|
||||
|
||||
// Go through all cs files, and find [Display] and [DisplayName] attributes
|
||||
foreach (var file in soldir.EnumerateFiles("*.cs", SearchOption.AllDirectories))
|
||||
{
|
||||
var txt = File.ReadAllText(file.FullName);
|
||||
var tree = CSharpSyntaxTree.ParseText(txt, new CSharpParseOptions(LanguageVersion.Default));
|
||||
var walker = new DisplayNameWalker();
|
||||
walker.Visit(tree.GetRoot());
|
||||
foreach (var k in walker.Keys)
|
||||
{
|
||||
defaultTranslatedKeys.Add(k);
|
||||
}
|
||||
}
|
||||
|
||||
// Go through all cshtml file, search for text-translate or ViewLocalizer usage
|
||||
using (var tester = CreateServerTester())
|
||||
{
|
||||
await tester.StartAsync();
|
||||
var engine = tester.PayTester.GetService<RazorProjectEngine>();
|
||||
foreach (var file in soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories))
|
||||
{
|
||||
var filePath = file.FullName;
|
||||
var txt = File.ReadAllText(file.FullName);
|
||||
if (txt.Contains("ViewLocalizer"))
|
||||
{
|
||||
var matches = Regex.Matches(txt, "ViewLocalizer\\[\"(.*?)\"[\\],]");
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
defaultTranslatedKeys.Add(match.Groups[1].Value);
|
||||
}
|
||||
}
|
||||
else if (txt.Contains("text-translate"))
|
||||
{
|
||||
filePath = filePath.Replace(Path.Combine(soldir.FullName, "BTCPayServer"), "/");
|
||||
var item = engine.FileSystem.GetItem(filePath);
|
||||
|
||||
var node = (DocumentIntermediateNode)engine.Process(item).Items[typeof(DocumentIntermediateNode)];
|
||||
foreach (var n in node.FindDescendantNodes<TagHelperIntermediateNode>())
|
||||
{
|
||||
foreach (var tagHelper in n.TagHelpers)
|
||||
{
|
||||
if (tagHelper.Name.EndsWith("TranslateTagHelper"))
|
||||
{
|
||||
var htmlContent = n.FindDescendantNodes<HtmlContentIntermediateNode>().First();
|
||||
var inner = txt.Substring(htmlContent.Source.Value.AbsoluteIndex, htmlContent.Source.Value.Length);
|
||||
defaultTranslatedKeys.Add(inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
defaultTranslatedKeys = defaultTranslatedKeys.Distinct().OrderBy(o => o).ToList();
|
||||
var path = Path.Combine(soldir.FullName, "BTCPayServer/Services/Translations.Default.cs");
|
||||
var defaultTranslation = File.ReadAllText(path);
|
||||
var startIdx = defaultTranslation.IndexOf("\"\"\"");
|
||||
var endIdx = defaultTranslation.LastIndexOf("\"\"\"");
|
||||
var content = defaultTranslation.Substring(0, startIdx + 3);
|
||||
content += "\n" + String.Join('\n', defaultTranslatedKeys) + "\n";
|
||||
content += defaultTranslation.Substring(endIdx);
|
||||
File.WriteAllText(path, content);
|
||||
}
|
||||
class DisplayNameWalker : CSharpSyntaxWalker
|
||||
{
|
||||
public List<string> Keys = new List<string>();
|
||||
public bool InAttribute = false;
|
||||
public override void VisitAttribute(AttributeSyntax node)
|
||||
{
|
||||
InAttribute = true;
|
||||
base.VisitAttribute(node);
|
||||
InAttribute = false;
|
||||
}
|
||||
public override void VisitIdentifierName(IdentifierNameSyntax node)
|
||||
{
|
||||
if (InAttribute)
|
||||
{
|
||||
InAttribute = node.Identifier.Text switch
|
||||
{
|
||||
"Display" => true,
|
||||
"DisplayAttribute" => true,
|
||||
"DisplayName" => true,
|
||||
"DisplayNameAttribute" => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
public override void VisitAttributeArgument(AttributeArgumentSyntax node)
|
||||
{
|
||||
if (InAttribute)
|
||||
{
|
||||
var name = node.Expression switch
|
||||
{
|
||||
LiteralExpressionSyntax les => les.Token.ValueText,
|
||||
IdentifierNameSyntax ins => ins.Identifier.Text,
|
||||
_ => throw new InvalidOperationException("Unknown node")
|
||||
};
|
||||
Keys.Add(name);
|
||||
InAttribute = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This utility will make sure that permission documentation is properly written in swagger.template.json
|
||||
/// </summary>
|
||||
|
|
|
@ -114,6 +114,9 @@
|
|||
<Watch Remove="Views\Shared\LocalhostBrowserSupport.cshtml" />
|
||||
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
|
||||
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
|
||||
<Watch Remove="Views\UIServer\CreateDictionary.cshtml" />
|
||||
<Watch Remove="Views\UIServer\EditDictionary.cshtml" />
|
||||
<Watch Remove="Views\UIServer\ListDictionaries.cshtml" />
|
||||
<Content Update="Views\UIApps\_ViewImports.cshtml">
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||
|
|
|
@ -309,6 +309,9 @@
|
|||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Branding" class="nav-link @ViewData.ActivePageClass(ServerNavPages.Branding)" asp-action="Branding">Branding</a>
|
||||
</li>
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
|
||||
<a asp-controller="UIServer" id="SectionNav-@ServerNavPages.Translations" class="nav-link @ViewData.ActivePageClass(ServerNavPages.Translations)" asp-action="ListDictionaries">Translations</a>
|
||||
</li>
|
||||
@if (BtcPayServerOptions.DockerDeployment)
|
||||
{
|
||||
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
|
|
108
BTCPayServer/Controllers/UIServerController.Translations.cs
Normal file
108
BTCPayServer/Controllers/UIServerController.Translations.cs
Normal file
|
@ -0,0 +1,108 @@
|
|||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Models.ServerViewModels;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIServerController
|
||||
{
|
||||
[HttpGet("server/dictionaries")]
|
||||
public async Task<IActionResult> ListDictionaries()
|
||||
{
|
||||
var dictionaries = await this._localizer.GetDictionaries();
|
||||
var vm = new ListDictionariesViewModel();
|
||||
foreach (var dictionary in dictionaries)
|
||||
{
|
||||
vm.Dictionaries.Add(new()
|
||||
{
|
||||
Editable = dictionary.Source == "Custom",
|
||||
Source = dictionary.Source,
|
||||
DictionaryName = dictionary.DictionaryName,
|
||||
Fallback = dictionary.Fallback,
|
||||
IsSelected =
|
||||
_policiesSettings.LangDictionary == dictionary.DictionaryName ||
|
||||
(_policiesSettings.LangDictionary is null && dictionary.Source == "Default")
|
||||
});
|
||||
}
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("server/dictionaries/create")]
|
||||
public async Task<IActionResult> CreateDictionary(string fallback = null)
|
||||
{
|
||||
var dictionaries = await this._localizer.GetDictionaries();
|
||||
return View(new CreateDictionaryViewModel()
|
||||
{
|
||||
Name = fallback is not null ? $"{fallback} (Copy)" : "",
|
||||
Fallback = fallback ?? Translations.DefaultLanguage,
|
||||
}.SetDictionaries(dictionaries));
|
||||
}
|
||||
[HttpPost("server/dictionaries/create")]
|
||||
public async Task<IActionResult> CreateDictionary(CreateDictionaryViewModel viewModel)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
try
|
||||
{
|
||||
await this._localizer.CreateDictionary(viewModel.Name, viewModel.Fallback, "Custom");
|
||||
}
|
||||
catch (DbException)
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.Name), $"'{viewModel.Name}' already exists");
|
||||
}
|
||||
}
|
||||
if (!ModelState.IsValid)
|
||||
return View(viewModel.SetDictionaries(await this._localizer.GetDictionaries()));
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Dictionary created";
|
||||
return RedirectToAction(nameof(EditDictionary), new { dictionary = viewModel.Name });
|
||||
}
|
||||
|
||||
[HttpGet("server/dictionaries/{dictionary}")]
|
||||
public async Task<IActionResult> EditDictionary(string dictionary)
|
||||
{
|
||||
if ((await this._localizer.GetDictionary(dictionary)) is null)
|
||||
return NotFound();
|
||||
var translations = await _localizer.GetTranslations(dictionary);
|
||||
return View(new EditDictionaryViewModel().SetTranslations(translations.Translations));
|
||||
}
|
||||
|
||||
[HttpPost("server/dictionaries/{dictionary}")]
|
||||
public async Task<IActionResult> EditDictionary(string dictionary, EditDictionaryViewModel viewModel)
|
||||
{
|
||||
var d = await this._localizer.GetDictionary(dictionary);
|
||||
if (d is null)
|
||||
return NotFound();
|
||||
if (!Translations.TryCreateFromText(viewModel.Translations, out var translations))
|
||||
{
|
||||
ModelState.AddModelError(nameof(viewModel.Translations), "Syntax error");
|
||||
return View(viewModel);
|
||||
}
|
||||
await _localizer.Save(d, translations);
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Dictionary updated";
|
||||
return RedirectToAction(nameof(ListDictionaries));
|
||||
}
|
||||
[HttpGet("server/dictionaries/{dictionary}/select")]
|
||||
public async Task<IActionResult> SelectDictionary(string dictionary)
|
||||
{
|
||||
var settings = await this._SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new();
|
||||
settings.LangDictionary = dictionary;
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
await _localizer.Load();
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Default dictionary changed to {dictionary}";
|
||||
return RedirectToAction(nameof(ListDictionaries));
|
||||
}
|
||||
[HttpPost("server/dictionaries/{dictionary}/delete")]
|
||||
public async Task<IActionResult> DeleteDictionary(string dictionary)
|
||||
{
|
||||
await _localizer.DeleteDictionary(dictionary);
|
||||
TempData[WellKnownTempData.SuccessMessage] = $"Dictionary {dictionary} deleted";
|
||||
return RedirectToAction(nameof(ListDictionaries));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -68,6 +68,7 @@ namespace BTCPayServer.Controllers
|
|||
private readonly UriResolver _uriResolver;
|
||||
private readonly EmailSenderFactory _emailSenderFactory;
|
||||
private readonly TransactionLinkProviders _transactionLinkProviders;
|
||||
private readonly LocalizerService _localizer;
|
||||
|
||||
public UIServerController(
|
||||
UserManager<ApplicationUser> userManager,
|
||||
|
@ -93,7 +94,8 @@ namespace BTCPayServer.Controllers
|
|||
EmailSenderFactory emailSenderFactory,
|
||||
IHostApplicationLifetime applicationLifetime,
|
||||
IHtmlHelper html,
|
||||
TransactionLinkProviders transactionLinkProviders
|
||||
TransactionLinkProviders transactionLinkProviders,
|
||||
LocalizerService localizer
|
||||
)
|
||||
{
|
||||
_policiesSettings = policiesSettings;
|
||||
|
@ -120,6 +122,7 @@ namespace BTCPayServer.Controllers
|
|||
ApplicationLifetime = applicationLifetime;
|
||||
Html = html;
|
||||
_transactionLinkProviders = transactionLinkProviders;
|
||||
_localizer = localizer;
|
||||
}
|
||||
|
||||
[HttpGet("server/stores")]
|
||||
|
@ -325,17 +328,22 @@ namespace BTCPayServer.Controllers
|
|||
|
||||
[Route("server/policies")]
|
||||
public async Task<IActionResult> Policies()
|
||||
{
|
||||
await UpdateViewBag();
|
||||
return View(_policiesSettings);
|
||||
}
|
||||
|
||||
private async Task UpdateViewBag()
|
||||
{
|
||||
ViewBag.UpdateUrlPresent = _Options.UpdateUrl != null;
|
||||
ViewBag.AppsList = await GetAppSelectList();
|
||||
return View(_policiesSettings);
|
||||
ViewBag.LangDictionaries = await GetLangDictionariesSelectList();
|
||||
}
|
||||
|
||||
[HttpPost("server/policies")]
|
||||
public async Task<IActionResult> Policies([FromServices] BTCPayNetworkProvider btcPayNetworkProvider, PoliciesSettings settings, string command = "")
|
||||
{
|
||||
ViewBag.UpdateUrlPresent = _Options.UpdateUrl != null;
|
||||
ViewBag.AppsList = await GetAppSelectList();
|
||||
await UpdateViewBag();
|
||||
|
||||
if (command == "add-domain")
|
||||
{
|
||||
|
@ -384,9 +392,12 @@ namespace BTCPayServer.Controllers
|
|||
domainToAppMappingItem.AppType = apps[domainToAppMappingItem.AppId];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
_ = _transactionLinkProviders.RefreshTransactionLinkTemplates();
|
||||
if (_policiesSettings.LangDictionary != settings.LangDictionary)
|
||||
await _localizer.Load();
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Policies updated successfully";
|
||||
return RedirectToAction(nameof(Policies));
|
||||
}
|
||||
|
@ -455,6 +466,12 @@ namespace BTCPayServer.Controllers
|
|||
return apps;
|
||||
}
|
||||
|
||||
private async Task<List<SelectListItem>> GetLangDictionariesSelectList()
|
||||
{
|
||||
var dictionaries = await this._localizer.GetDictionaries();
|
||||
return dictionaries.Select(d => new SelectListItem(d.DictionaryName, d.DictionaryName)).OrderBy(d => d.Value).ToList();
|
||||
}
|
||||
|
||||
private static bool TryParseAsExternalService(TorService torService, [MaybeNullWhen(false)] out ExternalService externalService)
|
||||
{
|
||||
externalService = null;
|
||||
|
|
|
@ -630,6 +630,7 @@ namespace BTCPayServer
|
|||
dataDirectories.StorageDir = Path.Combine(dataDirectories.DataDir, Storage.Services.Providers.FileSystemStorage.FileSystemFileProviderService.LocalStorageDirectoryName);
|
||||
dataDirectories.TempStorageDir = Path.Combine(dataDirectories.StorageDir, "tmp");
|
||||
dataDirectories.TempDir = Path.Combine(dataDirectories.DataDir, "tmp");
|
||||
dataDirectories.LangsDir = Path.Combine(dataDirectories.DataDir, "Langs");
|
||||
return dataDirectories;
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,10 @@ using ExchangeSharp;
|
|||
using Laraue.EfCoreTriggers.PostgreSql.Extensions;
|
||||
|
||||
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.AspNetCore.Mvc.Localization;
|
||||
|
||||
|
||||
#if ALTCOINS
|
||||
using BTCPayServer.Services.Altcoins.Monero;
|
||||
using BTCPayServer.Services.Altcoins.Zcash;
|
||||
|
@ -89,6 +93,11 @@ namespace BTCPayServer.Hosting
|
|||
}
|
||||
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration, Logs logs)
|
||||
{
|
||||
services.TryAddSingleton<IStringLocalizerFactory, LocalizerFactory>();
|
||||
services.TryAddSingleton<IHtmlLocalizerFactory, LocalizerFactory>();
|
||||
services.TryAddSingleton<LocalizerService>();
|
||||
services.TryAddSingleton<ViewLocalizer>();
|
||||
|
||||
services.AddSingleton<MvcNewtonsoftJsonOptions>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value);
|
||||
services.AddSingleton<JsonSerializerSettings>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value.SerializerSettings);
|
||||
services.AddDbContext<ApplicationDbContext>((provider, o) =>
|
||||
|
@ -161,6 +170,7 @@ namespace BTCPayServer.Hosting
|
|||
services.AddSingleton<IUIExtension>(new UIExtension("Lightning/ViewLightningLikePaymentData", "store-invoices-payments"));
|
||||
|
||||
services.AddStartupTask<BlockExplorerLinkStartupTask>();
|
||||
services.AddStartupTask<LoadTranslationsStartupTask>();
|
||||
services.TryAddSingleton<InvoiceRepository>();
|
||||
services.AddSingleton<PaymentService>();
|
||||
services.AddSingleton<BTCPayServerEnvironment>();
|
||||
|
|
86
BTCPayServer/Hosting/LoadTranslationsStartupTask.cs
Normal file
86
BTCPayServer/Hosting/LoadTranslationsStartupTask.cs
Normal file
|
@ -0,0 +1,86 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Configuration;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace BTCPayServer.Hosting
|
||||
{
|
||||
public class LoadTranslationsStartupTask : IStartupTask
|
||||
{
|
||||
public LoadTranslationsStartupTask(
|
||||
ILogger<LoadTranslationsStartupTask> logger,
|
||||
LocalizerService localizerService,
|
||||
IOptions<DataDirectories> dataDirectories)
|
||||
{
|
||||
DataDirectories = dataDirectories.Value;
|
||||
Logger = logger;
|
||||
LocalizerService = localizerService;
|
||||
}
|
||||
|
||||
public DataDirectories DataDirectories { get; }
|
||||
public ILogger<LoadTranslationsStartupTask> Logger { get; }
|
||||
public LocalizerService LocalizerService { get; }
|
||||
|
||||
class DictionaryFileMetadata
|
||||
{
|
||||
[JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
|
||||
[JsonProperty("hash")]
|
||||
public uint256 Hash { get; set; }
|
||||
}
|
||||
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// This load languages files from a [datadir]/Langs into the database
|
||||
// to make startup faster, we skip update if we see that files didn't changes
|
||||
// since the last time they got loaded.
|
||||
// We do this by comparing hashes of the current file, to the one stored in DB.
|
||||
if (Directory.Exists(DataDirectories.LangsDir))
|
||||
{
|
||||
var files = Directory.GetFiles(DataDirectories.LangsDir);
|
||||
if (files.Length > 0)
|
||||
{
|
||||
Logger.LogInformation("Loading language files...");
|
||||
var dictionaries = await LocalizerService.GetDictionaries();
|
||||
foreach (var file in Directory.GetFiles(DataDirectories.LangsDir))
|
||||
{
|
||||
var langName = Path.GetFileName(file);
|
||||
var dictionary = dictionaries.FirstOrDefault(d => d.DictionaryName == langName);
|
||||
if (dictionary is null)
|
||||
dictionary = await LocalizerService.CreateDictionary(langName, null, "File");
|
||||
if (dictionary.Source != "File")
|
||||
{
|
||||
Logger.LogWarning($"Impossible to load language '{langName}', as it is already existing in the database, not initially imported by a File");
|
||||
continue;
|
||||
}
|
||||
var savedHash = dictionary.Metadata.ToObject<DictionaryFileMetadata>().Hash;
|
||||
var translations = Translations.CreateFromText(File.ReadAllText(file));
|
||||
var currentHash = new uint256(SHA256.HashData(Encoding.UTF8.GetBytes(translations.ToTextFormat())));
|
||||
|
||||
if (savedHash != currentHash)
|
||||
{
|
||||
var newMetadata = (JObject)dictionary.Metadata.DeepClone();
|
||||
newMetadata["hash"] = currentHash.ToString();
|
||||
dictionary = dictionary with { Metadata = newMetadata };
|
||||
Logger.LogInformation($"Updating dictionary '{langName}'");
|
||||
await LocalizerService.Save(dictionary, translations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do not make startup longer for this
|
||||
_ = LocalizerService.Load();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ using Microsoft.Extensions.DependencyInjection;
|
|||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -168,6 +169,7 @@ namespace BTCPayServer.Hosting
|
|||
.AddNewtonsoftJson()
|
||||
.AddRazorRuntimeCompilation()
|
||||
.AddPlugins(services, Configuration, LoggerFactory, bootstrapServiceProvider)
|
||||
.AddDataAnnotationsLocalization()
|
||||
.AddControllersAsServices();
|
||||
|
||||
services.AddServerSideBlazor();
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace BTCPayServer.Models.AccountViewModels
|
|||
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
[Display(Name = "Password")]
|
||||
public string Password { get; set; }
|
||||
public string LoginCode { get; set; }
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels;
|
||||
public class CreateDictionaryViewModel
|
||||
{
|
||||
[Required(AllowEmptyStrings = false)]
|
||||
public string Name { get; set; }
|
||||
public string Fallback { get; set; }
|
||||
public SelectListItem[] DictionariesListItems { get; set; }
|
||||
|
||||
internal CreateDictionaryViewModel SetDictionaries(LocalizerService.Dictionary[] dictionaries)
|
||||
{
|
||||
var items = dictionaries.Select(d => new SelectListItem(d.DictionaryName, d.DictionaryName, d.DictionaryName == Fallback)).ToArray();
|
||||
DictionariesListItems = items;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
using BTCPayServer.Services;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels;
|
||||
|
||||
public class EditDictionaryViewModel
|
||||
{
|
||||
public string Translations { get; set; }
|
||||
public int Lines { get; set; }
|
||||
|
||||
internal EditDictionaryViewModel SetTranslations(Translations translations)
|
||||
{
|
||||
Translations = translations.ToTextFormat();
|
||||
Lines = translations.Records.Count;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BTCPayServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
|
||||
namespace BTCPayServer.Models.ServerViewModels
|
||||
{
|
||||
public class ListDictionariesViewModel
|
||||
{
|
||||
public class DictionaryViewModel
|
||||
{
|
||||
public string DictionaryName { get; set; }
|
||||
public string Fallback { get; set; }
|
||||
public string Source { get; set; }
|
||||
public bool Editable { get; set; }
|
||||
public bool IsSelected { get; set; }
|
||||
}
|
||||
|
||||
public List<DictionaryViewModel> Dictionaries = new List<DictionaryViewModel>();
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
public class GeneralSettingsViewModel
|
||||
{
|
||||
|
||||
[Display(Name = "Store ID")]
|
||||
[Display(Name = "Store Id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[Display(Name = "Store Name")]
|
||||
|
@ -45,7 +45,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
[Display(Name = "Add additional fee (network fee) to invoice …")]
|
||||
public NetworkFeeMode NetworkFeeMode { get; set; }
|
||||
|
||||
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
|
||||
[Display(Name = "Consider the invoice paid even if the paid amount is … % less than expected")]
|
||||
[Range(0, 100)]
|
||||
public double PaymentTolerance { get; set; }
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||
[Display(Name = "Add additional fee (network fee) to invoice …")]
|
||||
public NetworkFeeMode NetworkFeeMode { get; set; }
|
||||
|
||||
[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
|
||||
[Display(Name = "Consider the invoice paid even if the paid amount is … % less than expected")]
|
||||
[Range(0, 100)]
|
||||
public double PaymentTolerance { get; set; }
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
public class WalletPSBTCombineViewModel
|
||||
{
|
||||
public string OtherPSBT { get; set; }
|
||||
[Display(Name = "PSBT to combine with...")]
|
||||
[Display(Name = "PSBT to combine with…")]
|
||||
public string PSBT { get; set; }
|
||||
[Display(Name = "Upload PSBT from file...")]
|
||||
[Display(Name = "Upload PSBT from file…")]
|
||||
public IFormFile UploadedPSBTFile { get; set; }
|
||||
|
||||
public string BackUrl { get; set; }
|
||||
|
|
|
@ -32,7 +32,7 @@ namespace BTCPayServer.Models.WalletViewModels
|
|||
public string PSBT { get; set; }
|
||||
public List<string> Errors { get; set; } = new List<string>();
|
||||
|
||||
[Display(Name = "Upload PSBT from file")]
|
||||
[Display(Name = "Upload PSBT from file…")]
|
||||
public IFormFile UploadedPSBTFile { get; set; }
|
||||
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace BTCPayServer.Plugins.Shopify.Models
|
|||
[Display(Name = "Shop Name")]
|
||||
public string ShopName { get; set; }
|
||||
|
||||
[Display(Name = "Api Key")]
|
||||
[Display(Name = "API Key")]
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
[Display(Name = "Admin API access token")]
|
||||
|
|
130
BTCPayServer/Services/LocalizerFactory.cs
Normal file
130
BTCPayServer/Services/LocalizerFactory.cs
Normal file
|
@ -0,0 +1,130 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using BTCPayServer.Logging;
|
||||
using Microsoft.AspNetCore.Mvc.Localization;
|
||||
using Microsoft.AspNetCore.Mvc.TagHelpers;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class LocalizerFactory : IStringLocalizerFactory, IHtmlLocalizerFactory
|
||||
{
|
||||
internal readonly Logs _logs;
|
||||
private readonly LocalizerService _localizerService;
|
||||
|
||||
class StringLocalizer : IStringLocalizer, IHtmlLocalizer
|
||||
{
|
||||
private Type _resourceSource;
|
||||
private string _baseName;
|
||||
private string _location;
|
||||
private LocalizerFactory _Factory;
|
||||
|
||||
public StringLocalizer(LocalizerFactory factory, Type resourceSource)
|
||||
{
|
||||
_Factory = factory;
|
||||
_resourceSource = resourceSource;
|
||||
}
|
||||
Logs Logs => _Factory._logs;
|
||||
|
||||
|
||||
public StringLocalizer(LocalizerFactory jsonStringLocalizerFactory, string baseName, string location)
|
||||
{
|
||||
_Factory = jsonStringLocalizerFactory;
|
||||
_baseName = baseName;
|
||||
_location = location;
|
||||
}
|
||||
Translations Translations => _Factory._localizerService.Translations;
|
||||
public LocalizedString this[string name]
|
||||
{
|
||||
get
|
||||
{
|
||||
//Logs.PayServer.LogInformation($"this[name] with name:{name}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
|
||||
Translations.Records.TryGetValue(name, out var result);
|
||||
result = result ?? name;
|
||||
return new LocalizedString(name, result);
|
||||
}
|
||||
}
|
||||
|
||||
public LocalizedString this[string name, params object[] arguments]
|
||||
{
|
||||
get
|
||||
{
|
||||
//var args = String.Join(", ", arguments.Select((a, i) => $"arg[{i}]:{a}").ToArray());
|
||||
//Logs.PayServer.LogInformation($"this[name, arguments] with name:{name}, {args}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
|
||||
Translations.Records.TryGetValue(name, out var result);
|
||||
result = result ?? name;
|
||||
return new LocalizedString(name, string.Format(result, arguments));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
|
||||
{
|
||||
//Logs.PayServer.LogInformation($"GetAllStrings");
|
||||
return Translations.Records.Select(r => new LocalizedString(r.Key, r.Value));
|
||||
}
|
||||
|
||||
LocalizedHtmlString IHtmlLocalizer.this[string name]
|
||||
{
|
||||
get
|
||||
{
|
||||
//Logs.PayServer.LogInformation($"[HTML]: this[name] with name:{name}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
|
||||
Translations.Records.TryGetValue(name, out var result);
|
||||
result = result ?? name;
|
||||
return new LocalizedHtmlString(name, result);
|
||||
}
|
||||
}
|
||||
|
||||
LocalizedHtmlString IHtmlLocalizer.this[string name, params object[] arguments]
|
||||
{
|
||||
get
|
||||
{
|
||||
//var args = String.Join(", ", arguments.Select((a, i) => $"arg[{i}]:{a}").ToArray());
|
||||
//Logs.PayServer.LogInformation($"[HTML]:this[name, arguments] with name:{name}, {args}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
|
||||
Translations.Records.TryGetValue(name, out var result);
|
||||
result = result ?? name;
|
||||
return new LocalizedHtmlString(name, result, true, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
public LocalizedString GetString(string name)
|
||||
{
|
||||
//Logs.PayServer.LogInformation($"[HTML] GetString(name):");
|
||||
return this[name];
|
||||
}
|
||||
|
||||
public LocalizedString GetString(string name, params object[] arguments)
|
||||
{
|
||||
//var args = String.Join(", ", arguments.Select((a, i) => $"arg[{i}]:{a}").ToArray());
|
||||
Logs.PayServer.LogInformation($"[HTML] GetString(name,args):");
|
||||
return this[name, arguments];
|
||||
}
|
||||
}
|
||||
public LocalizerFactory(Logs logs, LocalizerService localizerService)
|
||||
{
|
||||
_logs = logs;
|
||||
_localizerService = localizerService;
|
||||
}
|
||||
public IStringLocalizer Create(Type resourceSource)
|
||||
{
|
||||
return new StringLocalizer(this, resourceSource);
|
||||
}
|
||||
|
||||
public IStringLocalizer Create(string baseName, string location)
|
||||
{
|
||||
return new StringLocalizer(this, baseName, location);
|
||||
}
|
||||
|
||||
IHtmlLocalizer IHtmlLocalizerFactory.Create(Type resourceSource)
|
||||
{
|
||||
return new StringLocalizer(this, resourceSource);
|
||||
}
|
||||
|
||||
IHtmlLocalizer IHtmlLocalizerFactory.Create(string baseName, string location)
|
||||
{
|
||||
return new StringLocalizer(this, baseName, location);
|
||||
}
|
||||
}
|
||||
}
|
170
BTCPayServer/Services/LocalizerService.cs
Normal file
170
BTCPayServer/Services/LocalizerService.cs
Normal file
|
@ -0,0 +1,170 @@
|
|||
#nullable enable
|
||||
using System.Collections;
|
||||
using System.Collections.Frozen;
|
||||
using Dapper;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static BTCPayServer.Services.LocalizerService;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public class LocalizerService
|
||||
{
|
||||
public LocalizerService(
|
||||
ILogger<LocalizerService> logger,
|
||||
ApplicationDbContextFactory contextFactory,
|
||||
ISettingsAccessor<PoliciesSettings> settingsAccessor)
|
||||
{
|
||||
_logger = logger;
|
||||
_ContextFactory = contextFactory;
|
||||
_settingsAccessor = settingsAccessor;
|
||||
_LoadedTranslations = new LoadedTranslations(Translations.Default, Translations.Default, Translations.DefaultLanguage);
|
||||
}
|
||||
|
||||
public record LoadedTranslations(Translations Translations, Translations Fallback, string LangName);
|
||||
LoadedTranslations _LoadedTranslations;
|
||||
public Translations Translations => _LoadedTranslations.Translations;
|
||||
|
||||
private readonly ILogger<LocalizerService> _logger;
|
||||
private readonly ApplicationDbContextFactory _ContextFactory;
|
||||
private readonly ISettingsAccessor<PoliciesSettings> _settingsAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Load the translation of the server into memory
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
_LoadedTranslations = await GetTranslations(_settingsAccessor.Settings.LangDictionary);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load translations");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LoadedTranslations> GetTranslations(string dictionaryName)
|
||||
{
|
||||
var ctx = _ContextFactory.CreateContext();
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
var all = await conn.QueryAsync<(bool fallback, string sentence, string? translation)>(
|
||||
"SELECT 'f'::BOOL fallback, sentence, translation FROM translations WHERE dict_id=@dict_id " +
|
||||
"UNION ALL " +
|
||||
"SELECT 't'::BOOL fallback, sentence, translation FROM translations WHERE dict_id=(SELECT fallback FROM lang_dictionaries WHERE dict_id=@dict_id)",
|
||||
new
|
||||
{
|
||||
dict_id = dictionaryName,
|
||||
});
|
||||
var fallback = new Translations(all.Where(a => a.fallback).Select(o => KeyValuePair.Create(o.sentence, o.translation)), Translations.Default);
|
||||
var translations = new Translations(all.Where(a => !a.fallback).Select(o => KeyValuePair.Create(o.sentence, o.translation)), fallback);
|
||||
return new LoadedTranslations(translations, fallback, dictionaryName);
|
||||
}
|
||||
|
||||
public async Task Save(Dictionary dictionary, Translations translations)
|
||||
{
|
||||
var loadedTranslations = await GetTranslations(dictionary.DictionaryName);
|
||||
translations = translations.WithFallback(loadedTranslations.Fallback);
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var diffs = loadedTranslations.Translations.CalculateDiff(translations);
|
||||
var conn = ctx.Database.GetDbConnection();
|
||||
List<string> keys = new List<string>();
|
||||
List<string> deletedKeys = new List<string>();
|
||||
List<string> values = new List<string>();
|
||||
|
||||
// The basic idea here is that we can remove from
|
||||
// the dictionary any translations which are the same
|
||||
// as the fallback. This way, if the fallback get updated,
|
||||
// it will also update the dictionary.
|
||||
foreach (var diff in diffs)
|
||||
{
|
||||
if (diff is Translations.Diff.Added a)
|
||||
{
|
||||
if (a.Value != loadedTranslations.Fallback[a.Key])
|
||||
{
|
||||
keys.Add(a.Key);
|
||||
values.Add(a.Value);
|
||||
}
|
||||
}
|
||||
else if (diff is Translations.Diff.Modified m)
|
||||
{
|
||||
if (m.NewValue != loadedTranslations.Fallback[m.Key])
|
||||
{
|
||||
keys.Add(m.Key);
|
||||
values.Add(m.NewValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
deletedKeys.Add(m.Key);
|
||||
}
|
||||
}
|
||||
else if (diff is Translations.Diff.Deleted d)
|
||||
{
|
||||
deletedKeys.Add(d.Key);
|
||||
}
|
||||
}
|
||||
await conn.ExecuteAsync("INSERT INTO lang_translations SELECT @dict_id, sentence, translation FROM unnest(@keys, @values) AS t(sentence, translation) ON CONFLICT (dict_id, sentence) DO UPDATE SET translation = EXCLUDED.translation; ",
|
||||
new
|
||||
{
|
||||
dict_id = loadedTranslations.LangName,
|
||||
keys = keys.ToArray(),
|
||||
values = values.ToArray()
|
||||
});
|
||||
await conn.ExecuteAsync("DELETE FROM lang_translations WHERE dict_id=@dict_id AND sentence=ANY(@keys)",
|
||||
new
|
||||
{
|
||||
dict_id = loadedTranslations.LangName,
|
||||
keys = deletedKeys.ToArray()
|
||||
});
|
||||
|
||||
if (_LoadedTranslations.LangName == loadedTranslations.LangName)
|
||||
_LoadedTranslations = loadedTranslations with { Translations = translations };
|
||||
}
|
||||
|
||||
public record Dictionary(string DictionaryName, string? Fallback, string Source, JObject Metadata);
|
||||
public async Task<Dictionary[]> GetDictionaries()
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var db = ctx.Database.GetDbConnection();
|
||||
var rows = await db.QueryAsync<(string dict_id, string? fallback, string? source, string? metadata)>("SELECT * FROM lang_dictionaries");
|
||||
return rows.Select(r => new Dictionary(r.dict_id, r.fallback, r.source ?? "", JObject.Parse(r.metadata ?? "{}"))).ToArray();
|
||||
}
|
||||
public async Task<Dictionary?> GetDictionary(string name)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var db = ctx.Database.GetDbConnection();
|
||||
var r = await db.QueryFirstAsync("SELECT * FROM lang_dictionaries WHERE dict_id=@dict_id", new { dict_id = name });
|
||||
if (r is null)
|
||||
return null;
|
||||
return new Dictionary(r.dict_id, r.fallback, r.source ?? "", JObject.Parse(r.metadata ?? "{}"));
|
||||
}
|
||||
|
||||
public async Task<Dictionary> CreateDictionary(string langName, string? fallback, string source)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var db = ctx.Database.GetDbConnection();
|
||||
await db.ExecuteAsync("INSERT INTO lang_dictionaries (dict_id, fallback, source) VALUES (@langName, @fallback, @source)", new { langName, fallback, source });
|
||||
return new Dictionary(langName, fallback, source ?? "", new JObject());
|
||||
}
|
||||
|
||||
public async Task DeleteDictionary(string dictionary)
|
||||
{
|
||||
await using var ctx = _ContextFactory.CreateContext();
|
||||
var db = ctx.Database.GetDbConnection();
|
||||
await db.ExecuteAsync("DELETE FROM lang_dictionaries WHERE dict_id=@dict_id AND source='Custom'", new { dict_id = dictionary });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,11 @@ namespace BTCPayServer.Services
|
|||
set { LockSubscription = !value; }
|
||||
}
|
||||
|
||||
[DefaultValue("English")]
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Backend's language")]
|
||||
public string LangDictionary { get; set; } = "English";
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[Display(Name = "Admin must approve new users")]
|
||||
public bool RequiresUserApproval { get; set; }
|
||||
|
|
242
BTCPayServer/Services/Translations.Default.cs
Normal file
242
BTCPayServer/Services/Translations.Default.cs
Normal file
|
@ -0,0 +1,242 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public partial class Translations
|
||||
{
|
||||
static Translations()
|
||||
{
|
||||
// Text generated by UpdateDefaultTranslations.
|
||||
// Please run it before release.
|
||||
var knownTranslations =
|
||||
"""
|
||||
Account key
|
||||
Account key path
|
||||
Add additional fee (network fee) to invoice …
|
||||
Add Exchange Rate Spread
|
||||
Add hop hints for private channels to the Lightning invoice
|
||||
Admin API access token
|
||||
Admin must approve new users
|
||||
Allow anyone to create invoice
|
||||
Allow form for public use
|
||||
Allow payee to create invoices with custom amounts
|
||||
Allow payee to pass a comment
|
||||
Allow Stores use the Server's SMTP email settings as their default
|
||||
Always include non-witness UTXO if available
|
||||
Amazon S3
|
||||
Amount
|
||||
API Key
|
||||
App
|
||||
App Name
|
||||
App Type
|
||||
Application
|
||||
Authenticator code
|
||||
Auto-detect language on checkout
|
||||
Automatically approve claims
|
||||
Available Payment Methods
|
||||
Azure Blob Storage
|
||||
Backend's language
|
||||
Batch size
|
||||
BIP39 Seed (12/24 word mnemonic phrase) or HD private key (xprv...)
|
||||
Brand Color
|
||||
Buyer Email
|
||||
Callback Notification URL
|
||||
Can use hot wallet
|
||||
Can use RPC import
|
||||
Celebrate payment with confetti
|
||||
Check releases on GitHub and notify when new BTCPay Server version is available
|
||||
Colors to rotate between with animation when a payment is made. One color per line.
|
||||
Confirm new password
|
||||
Confirm password
|
||||
Connection string
|
||||
Consider the invoice paid even if the paid amount is … % less than expected
|
||||
Consider the invoice settled when the payment transaction …
|
||||
Contact URL
|
||||
Contribution Perks Template
|
||||
Count all invoices created on the store as part of the goal
|
||||
Currency
|
||||
Current password
|
||||
Custom CSS
|
||||
Custom HTML title to display on Checkout page
|
||||
Custom sound file for successful payment
|
||||
Custom Theme Extension Type
|
||||
Custom Theme File
|
||||
Default currency
|
||||
Default language on checkout
|
||||
Default payment method on checkout
|
||||
Default role for users on a new store
|
||||
Derivation scheme
|
||||
Derivation scheme format
|
||||
Description
|
||||
Description template of the lightning invoice
|
||||
Destination Address
|
||||
Disable public user registration
|
||||
Disable stores from using the server's email settings as backup
|
||||
Discourage search engines from indexing this site
|
||||
Display app on website root
|
||||
Display contribution ranking
|
||||
Display contribution value
|
||||
Display item selection for keypad
|
||||
Display Lightning payment amounts in Satoshis
|
||||
Display the category list
|
||||
Display the search bar
|
||||
Display Title
|
||||
Disqus Shortname
|
||||
Do not allow additional contributions after target has been reached
|
||||
Does not extend a BTCPay Server theme, fully custom
|
||||
Domain
|
||||
Domain name
|
||||
Don't create UTXO change
|
||||
Email
|
||||
Email address
|
||||
Email confirmation required
|
||||
Email confirmed?
|
||||
Enable background animations on new payments
|
||||
Enable Disqus Comments
|
||||
Enable experimental features
|
||||
Enable LNURL
|
||||
Enable Payjoin/P2EP
|
||||
Enable public receipt page for settled invoices
|
||||
Enable public user registration
|
||||
Enable sounds on checkout page
|
||||
Enable sounds on new payments
|
||||
Enable tips
|
||||
End date
|
||||
Error
|
||||
Expiration Date
|
||||
Extends the BTCPay Server Dark theme
|
||||
Extends the BTCPay Server Light theme
|
||||
Featured Image URL
|
||||
Fee rate (sat/vB)
|
||||
Form configuration (JSON)
|
||||
Gap limit
|
||||
Google Cloud Storage
|
||||
GRPC SSL Cipher suite (GRPC_SSL_CIPHER_SUITES)
|
||||
Image
|
||||
Invoice currency
|
||||
Invoice expires if the full amount has not been paid after …
|
||||
Invoice metadata
|
||||
Is administrator?
|
||||
Is signing key
|
||||
Item Description
|
||||
Keypad
|
||||
Lightning node (LNURL Auth)
|
||||
LNURL Classic Mode
|
||||
Local File System
|
||||
Logo
|
||||
Make Crowdfund Public
|
||||
Master fingerprint
|
||||
Max sats
|
||||
Memo
|
||||
Metadata
|
||||
Min sats
|
||||
Minimum acceptable expiration time for BOLT11 for refunds
|
||||
New password
|
||||
Non-admins can access the User Creation API Endpoint
|
||||
Non-admins can create Hot Wallets for their Store
|
||||
Non-admins can import Hot Wallets for their Store
|
||||
Non-admins can use the Internal Lightning Node for their Store
|
||||
Non-admins cannot access the User Creation API Endpoint
|
||||
Notification Email
|
||||
Notification URL
|
||||
Only enable the payment method after user explicitly chooses it
|
||||
Optional seed passphrase
|
||||
Order Id
|
||||
Override the block explorers used
|
||||
Pair to
|
||||
Password
|
||||
Password (leave blank to generate invite-link)
|
||||
PayJoin BIP21
|
||||
Payment invalid if transactions fails to confirm … after invoice expiration
|
||||
Payout Methods
|
||||
Plugin server
|
||||
Point of Sale Style
|
||||
Policies
|
||||
Preferred Price Source
|
||||
Print display
|
||||
Product list
|
||||
Product list with cart
|
||||
Profile Picture
|
||||
PSBT content
|
||||
PSBT to combine with…
|
||||
Public Key
|
||||
Rate Rules
|
||||
Recommended fee confirmation target blocks
|
||||
Recovery Code
|
||||
Redirect invoice to redirect url automatically after paid
|
||||
Redirect URL
|
||||
Remember me
|
||||
Remember this machine
|
||||
Request contributor data on checkout
|
||||
Request customer data on checkout
|
||||
Reset goal every
|
||||
REST Uri
|
||||
Role
|
||||
Root fingerprint
|
||||
Scope
|
||||
Search engines can index this site
|
||||
Security device (FIDO2)
|
||||
Select the Default Currency during Store Creation
|
||||
Select the payout method used for refund
|
||||
Server Name
|
||||
Shop Name
|
||||
Show "Pay in wallet" button
|
||||
Show a timer … minutes before invoice expiration
|
||||
Show plugins in pre-release
|
||||
Show recommended fee
|
||||
Show the payment list in the public receipt page
|
||||
Show the QR code of the receipt in the public receipt page
|
||||
Show the store header
|
||||
Sign in
|
||||
Sort contribution perks by popularity
|
||||
Sounds to play when a payment is made. One sound per line
|
||||
Specify the amount and currency for the refund
|
||||
Start date
|
||||
Starting index
|
||||
Store
|
||||
Store Id
|
||||
Store Name
|
||||
Store Website
|
||||
Subtract fees from amount
|
||||
Support URL
|
||||
Supported Transaction Currencies
|
||||
Target Amount
|
||||
Test Email
|
||||
Text to display in the tip input
|
||||
Text to display on buttons allowing the user to enter a custom amount
|
||||
Text to display on each button for items with a specific price
|
||||
Tip percentage amounts (comma separated)
|
||||
Unify on-chain and lightning payment URL/QR code
|
||||
Upload PSBT from file…
|
||||
Url of the Dynamic DNS service you are using
|
||||
Use custom theme
|
||||
Use SSL
|
||||
User can input custom amount
|
||||
User can input discount in %
|
||||
UTXOs to spend from
|
||||
Verification Code
|
||||
Wallet file
|
||||
Wallet file content
|
||||
Welcome to {0}
|
||||
Your dynamic DNS hostname
|
||||
""";
|
||||
Default = Translations.CreateFromText(knownTranslations);
|
||||
Default = new Translations(new KeyValuePair<string, string>[]
|
||||
{
|
||||
// You can add additional hard coded default here
|
||||
// KeyValuePair.Create("key1", "value")
|
||||
// KeyValuePair.Create("key2", "value")
|
||||
}, Default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translations which are already in the Default aren't saved into database.
|
||||
/// This allows us to automatically update the english version if the translations didn't changed.
|
||||
///
|
||||
/// We only save into database the key/values that differ from Default
|
||||
/// </summary>
|
||||
public static Translations Default;
|
||||
public readonly static string DefaultLanguage = "English";
|
||||
}
|
||||
}
|
125
BTCPayServer/Services/Translations.cs
Normal file
125
BTCPayServer/Services/Translations.cs
Normal file
|
@ -0,0 +1,125 @@
|
|||
#nullable enable
|
||||
using System.Collections;
|
||||
using System.Collections.Frozen;
|
||||
using Dapper;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
{
|
||||
public partial class Translations : IEnumerable<KeyValuePair<string, string>>
|
||||
{
|
||||
public record Diff(string Key)
|
||||
{
|
||||
public record Deleted(string Key, string OldValue) : Diff(Key);
|
||||
public record Added(string Key, string Value) : Diff(Key);
|
||||
public record Modified(string Key, string NewValue, string OldValue) : Diff(Key);
|
||||
}
|
||||
public static bool TryCreateFromText(string text, [MaybeNullWhen(false)] out Translations translations)
|
||||
{
|
||||
translations = null;
|
||||
try
|
||||
{
|
||||
translations = CreateFromText(text);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public static Translations CreateFromText(string text)
|
||||
{
|
||||
text = (text ?? "").Replace("\r\n", "\n");
|
||||
var translations = new List<(string key, string? value)>();
|
||||
foreach (var line in text.Split("\n", StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var splitted = line.Split("=>", StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted is [var key, var value])
|
||||
{
|
||||
translations.Add((key, value));
|
||||
}
|
||||
else if (splitted is [var key2])
|
||||
{
|
||||
translations.Add((key2, key2));
|
||||
}
|
||||
}
|
||||
return new Translations(translations
|
||||
.Select(t => KeyValuePair.Create(t.key, t.value)));
|
||||
}
|
||||
|
||||
public Translations(IEnumerable<KeyValuePair<string, string?>> records) : this (records, null)
|
||||
{
|
||||
}
|
||||
public Translations(IEnumerable<KeyValuePair<string, string?>> records, Translations? fallback)
|
||||
{
|
||||
Dictionary<string, string> thisRecords = new Dictionary<string, string>();
|
||||
foreach (var r in records)
|
||||
{
|
||||
var v = r.Value?.Trim();
|
||||
if (string.IsNullOrEmpty(v))
|
||||
continue;
|
||||
thisRecords.TryAdd(r.Key.Trim(), v);
|
||||
}
|
||||
if (fallback is not null)
|
||||
{
|
||||
foreach (var r in fallback.Records)
|
||||
{
|
||||
thisRecords.TryAdd(r.Key, r.Value);
|
||||
}
|
||||
}
|
||||
Records = thisRecords.ToFrozenDictionary();
|
||||
}
|
||||
public readonly FrozenDictionary<string, string> Records;
|
||||
|
||||
public string? this[string? key] => key is null ? null : Records.TryGetValue(key, out var v) ? v : null;
|
||||
|
||||
public Diff[] CalculateDiff(Translations translations)
|
||||
{
|
||||
List<Diff> diff = new List<Diff>(translations.Records.Count + 10);
|
||||
foreach (var kv in translations)
|
||||
{
|
||||
if (Records.TryGetValue(kv.Key, out var oldValue))
|
||||
{
|
||||
if (oldValue != kv.Value)
|
||||
diff.Add(new Diff.Modified(kv.Key, kv.Value, oldValue));
|
||||
}
|
||||
else
|
||||
{
|
||||
diff.Add(new Diff.Added(kv.Key, kv.Value));
|
||||
}
|
||||
}
|
||||
foreach (var kv in this)
|
||||
{
|
||||
if (!translations.Records.ContainsKey(kv.Key))
|
||||
diff.Add(new Diff.Deleted(kv.Key, kv.Value));
|
||||
}
|
||||
return diff.ToArray();
|
||||
}
|
||||
|
||||
public Translations WithFallback(Translations? fallback)
|
||||
{
|
||||
return new Translations(this!, fallback);
|
||||
}
|
||||
public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
|
||||
{
|
||||
return Records.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public string ToTextFormat()
|
||||
{
|
||||
return string.Join('\n', Records.OrderBy(r => r.Key).Select(r => $"{r.Key} => {r.Value}").ToArray());
|
||||
}
|
||||
}
|
||||
}
|
55
BTCPayServer/TagHelpers/TranslateTagHelper.cs
Normal file
55
BTCPayServer/TagHelpers/TranslateTagHelper.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Services;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Localization;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BTCPayServer.TagHelpers
|
||||
{
|
||||
[HtmlTargetElement(Attributes = "text-translate")]
|
||||
[HtmlTargetElement(Attributes = "html-translate")]
|
||||
public class TranslateTagHelper : TagHelper
|
||||
{
|
||||
private readonly IStringLocalizer<TranslateTagHelper> _localizer;
|
||||
private readonly Safe _safe;
|
||||
|
||||
public bool TextTranslate { get; set; }
|
||||
public bool HtmlTranslate { get; set; }
|
||||
|
||||
|
||||
public TranslateTagHelper(
|
||||
IStringLocalizer<TranslateTagHelper> localizer,
|
||||
Safe safe)
|
||||
{
|
||||
_localizer = localizer;
|
||||
_safe = safe;
|
||||
}
|
||||
|
||||
|
||||
public override int Order
|
||||
{
|
||||
get
|
||||
{
|
||||
// Run this TagHelper before others
|
||||
return -10;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
var originalContent = output.Content.IsModified
|
||||
? output.Content.GetContent()
|
||||
: (await output.GetChildContentAsync()).GetContent();
|
||||
|
||||
var newContent = _localizer[originalContent];
|
||||
if (TextTranslate)
|
||||
output.Content.SetContent(newContent);
|
||||
else
|
||||
output.Content.SetHtmlContent(_safe.Raw(newContent.Value));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,7 +32,7 @@
|
|||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button id="Save" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
|
|
@ -29,8 +29,8 @@
|
|||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
|
||||
@if (Model.Archived)
|
||||
<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>
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<a class="btn btn-primary" role="button" id="CreateRole" asp-controller="@controller" asp-action="CreateOrEditRole" asp-route-role="create" asp-route-storeId="@storeId" permission="@permission">Add Role</a>
|
||||
<a id="page-primary" class="btn btn-primary" role="button" asp-controller="@controller" asp-action="CreateOrEditRole" asp-route-role="create" asp-route-storeId="@storeId" permission="@permission">Add Role</a>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
|
||||
<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>
|
||||
|
|
|
@ -8,4 +8,4 @@
|
|||
}
|
||||
}
|
||||
<partial name="_Form" model="@Model.Form" />
|
||||
<input type="submit" class="btn btn-primary" name="command" value="Submit" />
|
||||
<input id="page-primary" type="submit" class="btn btn-primary" name="command" value="Submit" />
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
<vc:main-logo />
|
||||
</a>
|
||||
|
||||
<h1 class="h2 mb-3">Welcome to @(string.IsNullOrWhiteSpace(settings.ServerName) ? "your BTCPay\u00a0Server" : settings.ServerName)</h1>
|
||||
<h1 class="h2 mb-3" text-translate="true">@ViewLocalizer["Welcome to {0}", string.IsNullOrWhiteSpace(settings.ServerName) ? "BTCPay Server" : settings.ServerName]</h1>
|
||||
@if (ViewBag.ShowLeadText)
|
||||
{
|
||||
<p class="lead">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
@inject BTCPayServer.Security.ContentSecurityPolicies Csp
|
||||
@inject BTCPayServer.Services.PoliciesSettings PoliciesSettings
|
||||
@{
|
||||
ViewData["Title"] = "Sign in";
|
||||
ViewData["Title"] = ViewLocalizer["Sign in"];
|
||||
Layout = "_LayoutSignedOut";
|
||||
Csp.UnsafeEval();
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
|||
<div class="form-group">
|
||||
<div class="d-flex justify-content-between">
|
||||
<label asp-for="Password" class="form-label"></label>
|
||||
<a asp-action="ForgotPassword" tabindex="-1">Forgot password?</a>
|
||||
<a asp-action="ForgotPassword" text-translate="true" tabindex="-1">Forgot password?</a>
|
||||
</div>
|
||||
<div class="input-group d-flex">
|
||||
<input asp-for="Password" class="form-control" required />
|
||||
|
@ -35,7 +35,7 @@
|
|||
</div>
|
||||
<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">Sign in</span></button>
|
||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="LoginButton"><span class="ps-3" 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">
|
||||
<vc:icon symbol="scan-qr" />
|
||||
</button>
|
||||
|
@ -49,7 +49,7 @@
|
|||
@if (!PoliciesSettings.LockSubscription)
|
||||
{
|
||||
<p class="text-center mt-2 mb-0">
|
||||
<a id="Register" style="font-size:1.15rem" asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]">Create your account</a>
|
||||
<a id="Register" style="font-size:1.15rem" asp-action="Register" asp-route-returnurl="@ViewData["ReturnUrl"]" text-translate="true">Create your account</a>
|
||||
</p>
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model BTCPayServer.Models.AccountViewModels.SetPasswordViewModel
|
||||
@model BTCPayServer.Models.AccountViewModels.SetPasswordViewModel
|
||||
@{
|
||||
var cta = Model.HasPassword ? "Reset your password" : "Create Account";
|
||||
ViewData["Title"] = cta;
|
||||
|
@ -39,5 +39,5 @@
|
|||
<input asp-for="ConfirmPassword" class="form-control" />
|
||||
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 btn-lg" id="SetPassword">@cta</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary w-100 btn-lg">@cta</button>
|
||||
</form>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model CreateAppViewModel
|
||||
@model CreateAppViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(AppsNavPages.Create, $"Create a new {Model.AppType ?? "app"}", Model.AppType);
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
|||
<form asp-action="CreateApp" asp-route-appType="@Model.AppType">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
|
||||
<input id="page-primary" type="submit" value="Create" class="btn btn-primary" />
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
</a>
|
||||
</small>
|
||||
</h2>
|
||||
<a asp-action="CreateApp" asp-route-storeId="@Context.GetStoreData().Id" class="btn btn-primary" role="button" id="CreateNewApp">Create a new app</a>
|
||||
<a id="page-primary" asp-action="CreateApp" asp-route-storeId="@Context.GetStoreData().Id" class="btn btn-primary" role="button">Create a new app</a>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive-md">
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
</h2>
|
||||
<a asp-action="Create" asp-route-storeId="@storeId" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreateForm" 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">
|
||||
Create Form
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -208,7 +208,7 @@
|
|||
</h2>
|
||||
</nav>
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveButton">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
|
||||
@if (!isNew)
|
||||
{
|
||||
<a class="btn btn-secondary" asp-action="ViewPublicForm" asp-route-formId="@formId" id="ViewForm">View</a>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
<h2>Welcome to @(string.IsNullOrWhiteSpace(settings.ServerName) ? "your BTCPay\u00a0Server" : settings.ServerName)</h2>
|
||||
<h2 text-translate="true">@ViewLocalizer["Welcome to {0}", string.IsNullOrWhiteSpace(settings.ServerName) ? "BTCPay Server" : settings.ServerName]</h2>
|
||||
|
||||
@if (!Model.HasStore)
|
||||
{
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<input type="submit" value="Create" class="btn btn-primary" id="Create" />
|
||||
<input id="page-primary" type="submit" value="Create" class="btn btn-primary" />
|
||||
</div>
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
</h2>
|
||||
<a id="CreateNewInvoice"
|
||||
<a id="page-primary"
|
||||
permission="@Policies.CanCreateInvoice"
|
||||
asp-action="CreateInvoice"
|
||||
asp-route-storeId="@Model.StoreId"
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<a data-bs-toggle="collapse" data-bs-target="#AddAddress" class="btn btn-primary" role="button">
|
||||
<a id="page-primary" data-bs-toggle="collapse" data-bs-target="#AddAddress" class="btn btn-primary" role="button">
|
||||
Add Address
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
|
||||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="Save" id="Save">Save</button>
|
||||
</nav>
|
||||
<button id="page-primary" name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row">
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<a class="btn btn-primary" asp-action="AddApiKey" id="AddApiKey">
|
||||
<a id="page-primary" class="btn btn-primary" asp-action="AddApiKey">
|
||||
Generate Key
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button type="submit" class="btn btn-primary" id="Generate">Generate API Key</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary">Generate API Key</button>
|
||||
</div>
|
||||
<p>Generate a new api key to use BTCPay through its API.</p>
|
||||
<div class="row">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model ChangePasswordViewModel
|
||||
@model ChangePasswordViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ManageNavPages.ChangePassword, "Change your password");
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
|||
<form method="post">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button type="submit" class="btn btn-primary" id="UpdatePassword">Update Password</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary">Update Password</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row">
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button type="submit" id="save" class="btn btn-primary">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="col-xxl-constrain col-xl-8">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<a class="btn btn-primary" id="regeneratecode" asp-action="LoginCodes">Regenerate code</a>
|
||||
<a id="page-primary" class="btn btn-primary" asp-action="LoginCodes">Regenerate code</a>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<p>Easily log into BTCPay Server on another device using a simple login code from an already authenticated device.</p>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="update">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="update">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model BTCPayServer.Models.ManageViewModels.SetPasswordViewModel
|
||||
@model BTCPayServer.Models.ManageViewModels.SetPasswordViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ManageNavPages.ChangePassword, "Set your password");
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
|||
<form method="post">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button type="submit" class="btn btn-primary">Set Password</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary">Set Password</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
|
||||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="Save" id="Save">Save</button>
|
||||
</nav>
|
||||
<button id="page-primary" name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row">
|
||||
|
|
|
@ -35,11 +35,11 @@
|
|||
<div>
|
||||
@if (string.IsNullOrEmpty(Model.Id))
|
||||
{
|
||||
<button type="submit" class="btn btn-primary" id="SaveButton" permission="@Policies.CanModifyPaymentRequests">Create</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" permission="@Policies.CanModifyPaymentRequests">Create</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveButton" permission="@Policies.CanModifyPaymentRequests">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1" permission="@Policies.CanModifyPaymentRequests">Save</button>
|
||||
<a class="btn btn-secondary" asp-action="ViewPaymentRequest" asp-route-payReqId="@Model.Id" id="ViewPaymentRequest" target="_blank">View</a>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<vc:icon symbol="info" />
|
||||
</a>
|
||||
</h2>
|
||||
<a asp-action="EditPaymentRequest" asp-route-storeId="@storeId" class="btn btn-primary mt-3 mt-sm-0" role="button" id="CreatePaymentRequest" permission="@Policies.CanModifyPaymentRequests">
|
||||
<a id="page-primary" asp-action="EditPaymentRequest" asp-route-storeId="@storeId" class="btn btn-primary mt-3 mt-sm-0" role="button" permission="@Policies.CanModifyPaymentRequests">
|
||||
Create Request
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -28,11 +28,11 @@
|
|||
<div>
|
||||
@if (string.IsNullOrEmpty(Model.Id))
|
||||
{
|
||||
<button type="submit" class="btn btn-primary" id="SaveButton">Create</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary">Create</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveButton">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary order-sm-1">Save</button>
|
||||
<a class="btn btn-secondary" asp-action="ViewPullPayment" asp-route-pullPaymentId="@Model.Id" id="ViewPullPayment" target="_blank">View</a>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</h2>
|
||||
<div>
|
||||
<a cheat-mode="true" class="btn btn-outline-info text-nowrap" asp-action="StoreReports" asp-route-fakeData="true" asp-route-viewName="@Model.Request?.ViewName">Create fake data</a>
|
||||
<button id="exportCSV" class="btn btn-primary text-nowrap" type="button">Export</button>
|
||||
<button id="page-primary" class="btn btn-primary text-nowrap" type="button">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-column flex-sm-row align-items-center gap-3 mb-l">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Services
|
||||
@model BrandingViewModel;
|
||||
@inject IFileService FileService
|
||||
|
@ -16,7 +16,7 @@
|
|||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
|
37
BTCPayServer/Views/UIServer/CreateDictionary.cshtml
Normal file
37
BTCPayServer/Views/UIServer/CreateDictionary.cshtml
Normal file
|
@ -0,0 +1,37 @@
|
|||
@using BTCPayServer.Abstractions.Models
|
||||
@model CreateDictionaryViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Translations);
|
||||
ViewData["Title"] = "Create a new dictionary";
|
||||
}
|
||||
<form method="post" class="d-flex flex-column">
|
||||
<div class="form-group">
|
||||
<div class="sticky-header">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a asp-action="ListDictionaries">Dictionaries</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
|
||||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<input id="page-primary" type="submit" value="Create" class="btn btn-primary" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<div class="form-group">
|
||||
<label asp-for="Name" class="form-label"></label>
|
||||
<input asp-for="Name" class="form-control" />
|
||||
<span asp-validation-for="Name" class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<label asp-for="Fallback" class="form-label"></label>
|
||||
<select asp-for="Fallback" class="form-select w-auto" asp-items="@Model.DictionariesListItems"></select>
|
||||
<span asp-validation-for="Fallback" class="text-danger"></span>
|
||||
<div class="form-text">If a translation isn’t available in the new dictionary, it will be searched in the fallback.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -7,7 +7,7 @@
|
|||
<form method="post">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Generate">Generate</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Generate">Generate</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model BTCPayServer.Controllers.RegisterFromAdminViewModel
|
||||
@model BTCPayServer.Controllers.RegisterFromAdminViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Users, "Create account");
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
|||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button id="Save" type="submit" class="btn btn-primary" name="command" value="Save">Create Account</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Create Account</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model BTCPayServer.Models.ServerViewModels.DynamicDnsViewModel
|
||||
@model BTCPayServer.Models.ServerViewModels.DynamicDnsViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Services, "Dynamic DNS Service");
|
||||
}
|
||||
|
@ -31,7 +31,7 @@
|
|||
</small>
|
||||
</h2>
|
||||
</nav>
|
||||
<button name="command" class="btn btn-primary" type="submit" value="Save">Save</button>
|
||||
<button id="page-primary" name="command" class="btn btn-primary" type="submit" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
</nav>
|
||||
<div>
|
||||
<form method="post" asp-action="DynamicDnsService">
|
||||
<button id="AddDynamicDNS" class="btn btn-primary mt-2" type="submit">Add Service</button>
|
||||
<button id="page-primary" class="btn btn-primary mt-2" type="submit">Add Service</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
49
BTCPayServer/Views/UIServer/EditDictionary.cshtml
Normal file
49
BTCPayServer/Views/UIServer/EditDictionary.cshtml
Normal file
|
@ -0,0 +1,49 @@
|
|||
@using BTCPayServer.Abstractions.Models
|
||||
@model EditDictionaryViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Translations);
|
||||
ViewData["Title"] = Context.GetRouteValue("dictionary");
|
||||
}
|
||||
|
||||
<form method="post" class="d-flex flex-column">
|
||||
<div class="sticky-header">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a asp-action="ListDictionaries">Dictionaries</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
|
||||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
<div class="d-flex mb-4">
|
||||
<div class="flex-fill">
|
||||
<p>
|
||||
|
||||
Translations are formatted as <b>KEY => TRANSLATION</b>; for example, <b>Welcome => Bienvenue</b> translates <b>Welcome</b> to <b>Bienvenue</b>.
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
To use the translation from this dictionary's fallback, you can:
|
||||
<ul>
|
||||
<li>Remove the translation from this dictionary.</li>
|
||||
<li>Set the translation to match the string in the fallback.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p class="mb-0">Please note that not all text is translatable, and future updates may modify existing translations or introduce new translatable phrases.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain d-flex flex-column">
|
||||
<div class="form-group">
|
||||
<label asp-for="Translations" class="form-label"></label>
|
||||
<textarea asp-for="Translations" class="form-control translation-editor" rows="@Model.Lines" cols="40"></textarea>
|
||||
<span asp-validation-for="Translations" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
|
@ -1,4 +1,4 @@
|
|||
@model ServerEmailsViewModel
|
||||
@model ServerEmailsViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Emails, "Emails");
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
|||
<form method="post" autocomplete="off">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save" id="Save">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="form-group mb-4">
|
||||
|
|
67
BTCPayServer/Views/UIServer/ListDictionaries.cshtml
Normal file
67
BTCPayServer/Views/UIServer/ListDictionaries.cshtml
Normal file
|
@ -0,0 +1,67 @@
|
|||
@using BTCPayServer.Abstractions.Models
|
||||
@model ListDictionariesViewModel
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Translations);
|
||||
ViewData["Title"] = "Dictionaries";
|
||||
}
|
||||
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<a id="page-primary" asp-action="CreateDictionary" class="btn btn-primary" role="button">
|
||||
Create
|
||||
</a>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
||||
<div class="d-flex mb-4">
|
||||
<div class="flex-fill">
|
||||
<p class="mb-0">
|
||||
Dictionaries enable you to translate the BTCPay Server backend into different languages.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Dictionary</th>
|
||||
<th>Fallback</th>
|
||||
<th class="actions-col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var v in Model.Dictionaries)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
@if (!v.Editable)
|
||||
{
|
||||
<span class="@(v.IsSelected? "fw-bold" : "")">@v.DictionaryName</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="@(v.IsSelected? "fw-bold" : "")" asp-action="EditDictionary" asp-route-dictionary="@v.DictionaryName">@v.DictionaryName</a>
|
||||
}
|
||||
</td>
|
||||
<td>@v.Fallback</td>
|
||||
<td class="actions-col">
|
||||
<div class="d-inline-flex align-items-center gap-3">
|
||||
<a asp-action="CreateDictionary" asp-route-fallback="@v.DictionaryName">Clone</a>
|
||||
@if (!v.IsSelected)
|
||||
{
|
||||
<a id="Select-@v.DictionaryName" asp-action="SelectDictionary" asp-route-dictionary="@v.DictionaryName">Select</a>
|
||||
}
|
||||
@if (v.Editable && !v.IsSelected)
|
||||
{
|
||||
<a id="Delete-@v.DictionaryName" asp-action="DeleteDictionary" asp-route-dictionary="@v.DictionaryName" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The dictionary <b>@Html.Encode(v.DictionaryName)</b> will be removed from this server." data-confirm-input="DELETE">Remove</a>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<partial name="_Confirm" model="@(new ConfirmModel("Delete dictionary", "This dictionary will be removed from this server.", "Delete"))" />
|
||||
</div>
|
|
@ -15,7 +15,7 @@
|
|||
}
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<a asp-action="CreateUser" class="btn btn-primary" role="button" id="CreateUser">
|
||||
<a id="page-primary" asp-action="CreateUser" class="btn btn-primary" role="button">
|
||||
Add User
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<form method="post">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button id="SaveButton" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
@ -143,6 +143,15 @@
|
|||
<span asp-validation-for="AllowSearchEngines" class="text-danger"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check my-3">
|
||||
<label asp-for="LangDictionary" class="form-label"></label>
|
||||
<select asp-for="LangDictionary"
|
||||
asp-items="@(new SelectList(ViewBag.LangDictionaries, nameof(SelectListItem.Value), nameof(SelectListItem.Text), Model.LangDictionary))"
|
||||
class="form-select">
|
||||
</select>
|
||||
<span asp-validation-for="LangDictionary" class="text-danger"></span>
|
||||
<div class="form-text">Add or customize translations <a asp-action="ListDictionaries">here</a>.</div>
|
||||
</div>
|
||||
@if (ViewBag.UpdateUrlPresent)
|
||||
{
|
||||
<div class="d-flex my-3">
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace BTCPayServer.Views.Server
|
|||
{
|
||||
Users, Emails, Policies, Branding, Services, Maintenance, Logs, Files, Plugins,
|
||||
Roles,
|
||||
Stores
|
||||
Stores,
|
||||
Translations
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save">Next</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Next</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model UsersViewModel.UserViewModel
|
||||
@inject IFileService FileService
|
||||
|
@ -17,8 +17,8 @@
|
|||
<li class="breadcrumb-item active" aria-current="page">User</li>
|
||||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="Save" id="SaveUser">Save</button>
|
||||
</nav>
|
||||
<button id="page-primary" name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="form-group">
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</ol>
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
</nav>
|
||||
<input type="submit" value="Create" class="btn btn-primary" id="Create"/>
|
||||
<input id="page-primary" type="submit" value="Create" class="btn btn-primary"/>
|
||||
</div>
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
<form method="post" enctype="multipart/form-data" permissioned="@Policies.CanModifyStoreSettings">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button type="submit" class="btn btn-primary" id="Save">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@model CreateTokenViewModel
|
||||
@model CreateTokenViewModel
|
||||
@{
|
||||
var store = Context.GetStoreData();
|
||||
ViewData.SetActivePage(StoreNavPages.Tokens, "Create New Token", store?.Id);
|
||||
|
@ -24,7 +24,7 @@
|
|||
{
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
}
|
||||
<input id="RequestPairing" type="submit" value="Request Pairing" class="btn btn-primary" />
|
||||
<input id="page-primary" type="submit" value="Request Pairing" class="btn btn-primary" />
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row">
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<form method="post" enctype="multipart/form-data" permissioned="@Policies.CanModifyStoreSettings">
|
||||
<div class="sticky-header">
|
||||
<h2>Store Settings</h2>
|
||||
<button type="submit" class="btn btn-primary" id="Save">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<form method="post">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button id="save" name="command" type="submit" value="save" class="btn btn-primary">Save</button>
|
||||
<button id="page-primary" name="command" type="submit" value="save" class="btn btn-primary">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row">
|
||||
|
|
|
@ -26,11 +26,11 @@
|
|||
</nav>
|
||||
@if (Model.IsNew)
|
||||
{
|
||||
<button name="add" type="submit" class="btn btn-primary" value="New" id="New">Add Webhook</button>
|
||||
<button id="page-primary" name="add" type="submit" class="btn btn-primary" value="New">Add Webhook</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button name="update" type="submit" class="btn btn-primary" value="Save" id="Save">Update Webhook</button>
|
||||
<button id="page-primary" name="update" type="submit" class="btn btn-primary" value="Save" >Update Webhook</button>
|
||||
}
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<form method="post" permissioned="@Policies.CanModifyStoreSettings">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||
<button id="page-primary" name="command" type="submit" class="btn btn-primary" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Abstractions.TagHelpers
|
||||
@using BTCPayServer.Client
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@model PairingModel
|
||||
|
@ -37,7 +37,7 @@
|
|||
{
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
}
|
||||
<button id="ApprovePairing" type="submit" class="btn btn-primary mt-3" title="Approve this pairing demand">Approve</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary mt-3" title="Approve this pairing demand">Approve</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row">
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
</div>
|
||||
|
||||
<div class="text-start mt-4">
|
||||
<button id="save" name="command" type="submit" value="save" class="btn btn-primary me-2">Save</button>
|
||||
<button id="page-primary" name="command" type="submit" value="save" class="btn btn-primary me-2">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<form method="post" autocomplete="off" permissioned="@Policies.CanModifyStoreSettings">
|
||||
<div class="sticky-header">
|
||||
<h2>Email Server</h2>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="Save" id="Save">Save</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save">Save</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
@if (Model.IsFallbackSetup())
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<form method="post">
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<button type="submit" class="btn btn-primary mt-3">Send test webhook</button>
|
||||
<button id="page-primary" type="submit" class="btn btn-primary mt-3">Send test webhook</button>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<div class="row">
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<div class="sticky-header">
|
||||
<h2>@ViewData["Title"]</h2>
|
||||
<a id="CreateWebhook" asp-action="NewWebhook" class="btn btn-primary" role="button" asp-route-storeId="@Context.GetRouteValue("storeId")" permission="@Policies.CanModifyStoreSettings">
|
||||
<a id="page-primary" asp-action="NewWebhook" class="btn btn-primary" role="button" asp-route-storeId="@Context.GetRouteValue("storeId")" permission="@Policies.CanModifyStoreSettings">
|
||||
Create Webhook
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
@using BTCPayServer.Data
|
||||
@using Microsoft.AspNetCore.Routing;
|
||||
@using BTCPayServer.Abstractions.Extensions;
|
||||
@inject Microsoft.AspNetCore.Mvc.Localization.ViewLocalizer ViewLocalizer
|
||||
@inject Safe Safe
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, BTCPayServer
|
||||
|
|
Loading…
Add table
Reference in a new issue