mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 05:54:26 +01:00
Implement store templates (#6704)
* Implement store templates * Use the template for the default rate rules * Polish messages * Do not show exchange selection if template has a script
This commit is contained in:
@@ -17,7 +17,7 @@ namespace BTCPayServer.Client.Models
|
||||
public string Website { get; set; }
|
||||
|
||||
public string BrandColor { get; set; }
|
||||
public bool ApplyBrandColorToBackend { get; set; }
|
||||
public bool? ApplyBrandColorToBackend { get; set; }
|
||||
public string LogoUrl { get; set; }
|
||||
public string CssUrl { get; set; }
|
||||
public string PaymentSoundUrl { get; set; }
|
||||
@@ -26,56 +26,56 @@ namespace BTCPayServer.Client.Models
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan InvoiceExpiration { get; set; } = TimeSpan.FromMinutes(15);
|
||||
public TimeSpan? InvoiceExpiration { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan DisplayExpirationTimer { get; set; } = TimeSpan.FromMinutes(5);
|
||||
public TimeSpan? DisplayExpirationTimer { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Days))]
|
||||
[JsonProperty("refundBOLT11Expiration", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan RefundBOLT11Expiration { get; set; } = TimeSpan.FromDays(30);
|
||||
public TimeSpan? RefundBOLT11Expiration { get; set; }
|
||||
|
||||
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public TimeSpan MonitoringExpiration { get; set; } = TimeSpan.FromMinutes(60);
|
||||
public TimeSpan? MonitoringExpiration { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public SpeedPolicy SpeedPolicy { get; set; }
|
||||
public SpeedPolicy? SpeedPolicy { get; set; }
|
||||
public string LightningDescriptionTemplate { get; set; }
|
||||
public double PaymentTolerance { get; set; } = 0;
|
||||
public bool AnyoneCanCreateInvoice { get; set; }
|
||||
public double? PaymentTolerance { get; set; }
|
||||
public bool? AnyoneCanCreateInvoice { get; set; }
|
||||
public string DefaultCurrency { get; set; }
|
||||
|
||||
public bool LightningAmountInSatoshi { get; set; }
|
||||
public bool LightningPrivateRouteHints { get; set; }
|
||||
public bool OnChainWithLnInvoiceFallback { get; set; }
|
||||
public bool LazyPaymentMethods { get; set; }
|
||||
public bool RedirectAutomatically { get; set; }
|
||||
public bool? LightningAmountInSatoshi { get; set; }
|
||||
public bool? LightningPrivateRouteHints { get; set; }
|
||||
public bool? OnChainWithLnInvoiceFallback { get; set; }
|
||||
public bool? LazyPaymentMethods { get; set; }
|
||||
public bool? RedirectAutomatically { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool Archived { get; set; }
|
||||
public bool? Archived { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool ShowRecommendedFee { get; set; } = true;
|
||||
public bool? ShowRecommendedFee { get; set; }
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public int RecommendedFeeBlockTarget { get; set; } = 1;
|
||||
public int? RecommendedFeeBlockTarget { get; set; }
|
||||
|
||||
public string DefaultPaymentMethod { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string DefaultLang { get; set; } = "en";
|
||||
public string DefaultLang { get; set; }
|
||||
|
||||
public string HtmlTitle { get; set; }
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public NetworkFeeMode NetworkFeeMode { get; set; } = NetworkFeeMode.Never;
|
||||
public NetworkFeeMode? NetworkFeeMode { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<PaymentMethodCriteriaData> PaymentMethodCriteria { get; set; }
|
||||
|
||||
public bool PayJoinEnabled { get; set; }
|
||||
public bool? PayJoinEnabled { get; set; }
|
||||
|
||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
|
||||
public bool? AutoDetectLanguage { get; set; }
|
||||
|
||||
45
BTCPayServer.Data/Migrations/20250501000000_storetemplate.cs
Normal file
45
BTCPayServer.Data/Migrations/20250501000000_storetemplate.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using BTCPayServer.Data;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BTCPayServer.Migrations
|
||||
{
|
||||
[DbContext(typeof(ApplicationDbContext))]
|
||||
[Migration("20250501000000_storetemplate")]
|
||||
public partial class storetemplate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// This migrates the old `Default Currency` settings from Server Settings to a Default Store Template.
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
UPDATE "Settings"
|
||||
SET "Value" =
|
||||
jsonb_set(
|
||||
-- remove "DefaultCurrency", create a template
|
||||
("Value" - 'DefaultCurrency') || '{"DefaultStoreTemplate":{"blob":{}}}'::JSONB,
|
||||
-- path to insert the new nested value
|
||||
'{DefaultStoreTemplate,blob,defaultCurrency}',
|
||||
-- extract the old value of "DefaultCurrency"
|
||||
to_jsonb("Value"->'DefaultCurrency'),
|
||||
true
|
||||
)
|
||||
WHERE "Id" = 'BTCPayServer.Services.PoliciesSettings'
|
||||
AND "Value" ? 'DefaultCurrency' AND "Value"->>'DefaultCurrency' != 'USD';
|
||||
|
||||
UPDATE "Settings"
|
||||
SET "Value" = "Value" - 'DefaultCurrency'
|
||||
WHERE "Id" = 'BTCPayServer.Services.PoliciesSettings'
|
||||
AND "Value" ? 'DefaultCurrency';
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,4 @@ public enum RateSource
|
||||
Coingecko,
|
||||
Direct
|
||||
}
|
||||
public record RateSourceInfo(string Id, string DisplayName, string Url, RateSource Source = RateSource.Direct);
|
||||
public record RateSourceInfo(string? Id, string DisplayName, string Url, RateSource Source = RateSource.Direct);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
@@ -15,7 +14,6 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Playwright;
|
||||
using NBitcoin;
|
||||
using NBitcoin.RPC;
|
||||
using OpenQA.Selenium;
|
||||
using Xunit;
|
||||
|
||||
namespace BTCPayServer.Tests
|
||||
@@ -42,8 +40,6 @@ namespace BTCPayServer.Tests
|
||||
await Server.StartAsync();
|
||||
var builder = new ConfigurationBuilder();
|
||||
builder.AddUserSecrets("AB0AC1DD-9D26-485B-9416-56A33F268117");
|
||||
var config = builder.Build();
|
||||
|
||||
var playwright = await Playwright.CreateAsync();
|
||||
Browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
|
||||
{
|
||||
@@ -93,7 +89,7 @@ namespace BTCPayServer.Tests
|
||||
}
|
||||
else
|
||||
{
|
||||
await GoToUrl(storeId == null ? "/invoices/" : $"/stores/{storeId}/invoices/");
|
||||
await GoToUrl($"/stores/{storeId}/invoices/");
|
||||
StoreId = storeId;
|
||||
}
|
||||
}
|
||||
@@ -123,9 +119,9 @@ namespace BTCPayServer.Tests
|
||||
await Page.SelectOptionAsync("select[name='DefaultPaymentMethod']", new SelectOptionValue { Value = defaultPaymentMethod });
|
||||
await ClickPagePrimary();
|
||||
|
||||
var statusText = (await FindAlertMessage(expectedSeverity)).TextContentAsync();
|
||||
var statusText = await (await FindAlertMessage(expectedSeverity)).TextContentAsync();
|
||||
var inv = expectedSeverity == StatusMessageModel.StatusSeverity.Success
|
||||
? Regex.Match(await statusText, @"Invoice (\w+) just created!").Groups[1].Value
|
||||
? Regex.Match(statusText!, @"Invoice (\w+) just created!").Groups[1].Value
|
||||
: null;
|
||||
|
||||
InvoiceId = inv;
|
||||
@@ -225,20 +221,28 @@ namespace BTCPayServer.Tests
|
||||
return new TestAccount(Server) { StoreId = StoreId, Email = CreatedUser, Password = Password, RegisterDetails = new Models.AccountViewModels.RegisterViewModel() { Password = "123456", Email = CreatedUser }, IsAdmin = IsAdmin };
|
||||
}
|
||||
|
||||
public async Task<(string storeName, string storeId)> CreateNewStore(bool keepId = true)
|
||||
public async Task<(string storeName, string storeId)> CreateNewStore(bool keepId = true, string preferredExchange = "CoinGecko")
|
||||
{
|
||||
if (await Page.Locator("#StoreSelectorToggle").IsVisibleAsync())
|
||||
if (!Page.Url.EndsWith("stores/create"))
|
||||
{
|
||||
await Page.Locator("#StoreSelectorToggle").ClickAsync();
|
||||
if (await Page.Locator("#StoreSelectorToggle").IsVisibleAsync())
|
||||
{
|
||||
await Page.ClickAsync("#StoreSelectorToggle");
|
||||
await Page.ClickAsync("#StoreSelectorCreate");
|
||||
}
|
||||
else
|
||||
{
|
||||
await GoToUrl("/stores/create");
|
||||
}
|
||||
}
|
||||
await GoToUrl("/stores/create");
|
||||
|
||||
var name = "Store" + RandomUtils.GetUInt64();
|
||||
TestLogs.LogInformation($"Created store {name}");
|
||||
await Page.FillAsync("#Name", name);
|
||||
|
||||
var selectedOption = await Page.Locator("#PreferredExchange option:checked").TextContentAsync();
|
||||
Assert.Equal("Recommendation (Kraken)", selectedOption.Trim());
|
||||
await Page.Locator("#PreferredExchange").SelectOptionAsync(new SelectOptionValue { Label = "CoinGecko" });
|
||||
Assert.Equal("Recommendation (Kraken)", selectedOption?.Trim());
|
||||
await Page.Locator("#PreferredExchange").SelectOptionAsync(new SelectOptionValue { Label = preferredExchange });
|
||||
await Page.ClickAsync("#Create");
|
||||
await Page.ClickAsync("#StoreNav-General");
|
||||
var storeId = await Page.InputValueAsync("#Id");
|
||||
@@ -471,7 +475,7 @@ namespace BTCPayServer.Tests
|
||||
walletId ??= WalletId;
|
||||
await GoToWallet(walletId, WalletsNavPages.Receive);
|
||||
var addressStr = await Page.Locator("#Address").GetAttributeAsync("data-text");
|
||||
var address = BitcoinAddress.Create(addressStr, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||
var address = BitcoinAddress.Create(addressStr!, ((BTCPayNetwork)Server.NetworkProvider.GetNetwork(walletId.CryptoCode)).NBitcoinNetwork);
|
||||
for (var i = 0; i < coins; i++)
|
||||
{
|
||||
bool mined = false;
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Models;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
@@ -12,6 +13,7 @@ using BTCPayServer.Views.Stores;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Playwright;
|
||||
using NBitcoin;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@@ -38,7 +40,6 @@ namespace BTCPayServer.Tests
|
||||
Assert.Contains("Starting listening NBXplorer", await s.Page.ContentAsync());
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task CanUseForms()
|
||||
{
|
||||
@@ -489,5 +490,92 @@ namespace BTCPayServer.Tests
|
||||
Assert.DoesNotContain("Configured", await s.Page.ContentAsync());
|
||||
Assert.Contains("test_fix", await s.Page.ContentAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanUseStoreTemplate()
|
||||
{
|
||||
await using var s = CreatePlaywrightTester(newDb: true);
|
||||
await s.StartAsync();
|
||||
await s.RegisterNewUser(true);
|
||||
await s.CreateNewStore(preferredExchange: "Kraken");
|
||||
var client = await s.AsTestAccount().CreateClient();
|
||||
await client.UpdateStore(s.StoreId, new UpdateStoreRequest()
|
||||
{
|
||||
Name = "Can Use Store?",
|
||||
Website = "https://test.com/",
|
||||
CelebratePayment = false,
|
||||
DefaultLang = "fr-FR",
|
||||
NetworkFeeMode = NetworkFeeMode.MultiplePaymentsOnly,
|
||||
ShowStoreHeader = false
|
||||
});
|
||||
await s.GoToServer();
|
||||
await s.Page.ClickAsync("#SetTemplate");
|
||||
await s.FindAlertMessage();
|
||||
|
||||
var newStore = await client.CreateStore(new ());
|
||||
Assert.Equal("Can Use Store?", newStore.Name);
|
||||
Assert.Equal("https://test.com/", newStore.Website);
|
||||
Assert.False(newStore.CelebratePayment);
|
||||
Assert.Equal("fr-FR", newStore.DefaultLang);
|
||||
Assert.Equal(NetworkFeeMode.MultiplePaymentsOnly, newStore.NetworkFeeMode);
|
||||
Assert.False(newStore.ShowStoreHeader);
|
||||
|
||||
newStore = await client.CreateStore(new (){ Name = "Yes you can also customize"});
|
||||
Assert.Equal("Yes you can also customize", newStore.Name);
|
||||
Assert.Equal("https://test.com/", newStore.Website);
|
||||
Assert.False(newStore.CelebratePayment);
|
||||
Assert.Equal("fr-FR", newStore.DefaultLang);
|
||||
Assert.Equal(NetworkFeeMode.MultiplePaymentsOnly, newStore.NetworkFeeMode);
|
||||
Assert.False(newStore.ShowStoreHeader);
|
||||
|
||||
await s.GoToUrl("/stores/create");
|
||||
Assert.Equal("Can Use Store?" ,await s.Page.InputValueAsync("#Name"));
|
||||
await s.Page.FillAsync("#Name", "Just changed it!");
|
||||
await s.Page.ClickAsync("#Create");
|
||||
await s.Page.ClickAsync("#StoreNav-General");
|
||||
var newStoreId = await s.Page.InputValueAsync("#Id");
|
||||
Assert.NotEqual(newStoreId, s.StoreId);
|
||||
|
||||
newStore = await client.GetStore(newStoreId);
|
||||
Assert.Equal("Just changed it!", newStore.Name);
|
||||
Assert.Equal("https://test.com/", newStore.Website);
|
||||
Assert.False(newStore.CelebratePayment);
|
||||
Assert.Equal("fr-FR", newStore.DefaultLang);
|
||||
Assert.Equal(NetworkFeeMode.MultiplePaymentsOnly, newStore.NetworkFeeMode);
|
||||
Assert.False(newStore.ShowStoreHeader);
|
||||
|
||||
await s.GoToServer();
|
||||
await s.Page.ClickAsync("#ResetTemplate");
|
||||
await s.FindAlertMessage(partialText: "Store template successfully unset");
|
||||
|
||||
await s.GoToUrl("/stores/create");
|
||||
Assert.Equal("" ,await s.Page.InputValueAsync("#Name"));
|
||||
|
||||
newStore = await client.CreateStore(new (){ Name = "Test"});
|
||||
Assert.Equal(TimeSpan.FromDays(30), newStore.RefundBOLT11Expiration);
|
||||
Assert.Equal(TimeSpan.FromDays(1), newStore.MonitoringExpiration);
|
||||
Assert.Equal(TimeSpan.FromMinutes(5), newStore.DisplayExpirationTimer);
|
||||
Assert.Equal(TimeSpan.FromMinutes(15), newStore.InvoiceExpiration);
|
||||
|
||||
// What happens if the default template doesn't have all the fields?
|
||||
var settings = s.Server.PayTester.GetService<SettingsRepository>();
|
||||
var policies = await settings.GetSettingAsync<PoliciesSettings>() ?? new();
|
||||
policies.DefaultStoreTemplate = new JObject()
|
||||
{
|
||||
["blob"] = new JObject()
|
||||
{
|
||||
["defaultCurrency"] = "AAA",
|
||||
["defaultLang"] = "de-DE"
|
||||
}
|
||||
};
|
||||
await settings.UpdateSetting(policies);
|
||||
newStore = await client.CreateStore(new() { Name = "Test2"});
|
||||
Assert.Equal("AAA", newStore.DefaultCurrency);
|
||||
Assert.Equal("de-DE", newStore.DefaultLang);
|
||||
Assert.Equal(TimeSpan.FromDays(30), newStore.RefundBOLT11Expiration);
|
||||
Assert.Equal(TimeSpan.FromDays(1), newStore.MonitoringExpiration);
|
||||
Assert.Equal(TimeSpan.FromMinutes(5), newStore.DisplayExpirationTimer);
|
||||
Assert.Equal(TimeSpan.FromMinutes(15), newStore.InvoiceExpiration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1449,7 +1449,8 @@ namespace BTCPayServer.Tests
|
||||
|
||||
var script = await tester.Page.InputValueAsync($"#{source}_Script");
|
||||
var defaultScript = await tester.Page.GetAttributeAsync($"#{source}_DefaultScript", "data-defaultScript");
|
||||
Assert.Equal(script, defaultScript);
|
||||
Assert.Contains("X_JPY = bitbank(X_JPY);", defaultScript);
|
||||
Assert.Contains("X_TRY = btcturk(X_TRY);", defaultScript);
|
||||
|
||||
await Test("BTC_JPY");
|
||||
rules = await tester.Page.Locator(".testresult .testresult_rule").AllAsync();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Amazon.Runtime.Internal;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
@@ -19,7 +19,9 @@ using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
|
||||
namespace BTCPayServer.Controllers.Greenfield
|
||||
@@ -34,12 +36,14 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly IFileService _fileService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
private readonly JsonSerializerSettings _serializedSettings;
|
||||
|
||||
public GreenfieldStoresController(
|
||||
StoreRepository storeRepository,
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
IFileService fileService,
|
||||
IOptions<MvcNewtonsoftJsonOptions> jsonOptions,
|
||||
UriResolver uriResolver)
|
||||
{
|
||||
_storeRepository = storeRepository;
|
||||
@@ -47,6 +51,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
_userManager = userManager;
|
||||
_fileService = fileService;
|
||||
_uriResolver = uriResolver;
|
||||
_serializedSettings = jsonOptions.Value.SerializerSettings;
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanViewStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
@@ -77,7 +82,7 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null) return StoreNotFound();
|
||||
|
||||
await _storeRepository.RemoveStore(storeId, _userManager.GetUserId(User));
|
||||
await _storeRepository.RemoveStore(storeId, _userManager.GetUserId(User) ?? "");
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -85,27 +90,39 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettingsUnscoped, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
public async Task<IActionResult> CreateStore(CreateStoreRequest request)
|
||||
{
|
||||
var store = await _storeRepository.GetDefaultStoreTemplate();
|
||||
request = await MergeStoreRequestWithTemplate(request, store);
|
||||
var validationResult = Validate(request);
|
||||
if (validationResult != null) return validationResult;
|
||||
|
||||
var store = new StoreData();
|
||||
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId);
|
||||
ToModel(request, store, defaultPaymentMethodId);
|
||||
await _storeRepository.CreateStore(_userManager.GetUserId(User), store);
|
||||
ToModel(request, store);
|
||||
await _storeRepository.CreateStore(_userManager.GetUserId(User) ?? "", store);
|
||||
return Ok(await FromModel(store));
|
||||
}
|
||||
|
||||
private async Task<T> MergeStoreRequestWithTemplate<T>(T request, StoreData store) where T: StoreBaseData
|
||||
{
|
||||
var serializer = JsonSerializer.Create(_serializedSettings);
|
||||
var requestRaw = JObject.FromObject(request, serializer);
|
||||
var templateRaw = JObject.FromObject(await FromModel(store), serializer);
|
||||
templateRaw.Merge(requestRaw,
|
||||
new()
|
||||
{
|
||||
MergeNullValueHandling = MergeNullValueHandling.Ignore,
|
||||
MergeArrayHandling = MergeArrayHandling.Replace
|
||||
});
|
||||
return templateRaw.ToObject<T>(serializer);
|
||||
}
|
||||
|
||||
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Greenfield)]
|
||||
[HttpPut("~/api/v1/stores/{storeId}")]
|
||||
public async Task<IActionResult> UpdateStore(string storeId, UpdateStoreRequest request)
|
||||
{
|
||||
var store = HttpContext.GetStoreData();
|
||||
if (store == null) return StoreNotFound();
|
||||
request = await MergeStoreRequestWithTemplate(request, store);
|
||||
var validationResult = Validate(request);
|
||||
if (validationResult != null) return validationResult;
|
||||
|
||||
PaymentMethodId.TryParse(request.DefaultPaymentMethod, out var defaultPaymentMethodId);
|
||||
ToModel(request, store, defaultPaymentMethodId);
|
||||
ToModel(request, store);
|
||||
await _storeRepository.UpdateStore(store);
|
||||
return Ok(await FromModel(store));
|
||||
}
|
||||
@@ -223,13 +240,16 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
};
|
||||
}
|
||||
|
||||
private void ToModel(StoreBaseData restModel, StoreData model, PaymentMethodId defaultPaymentMethod)
|
||||
[SuppressMessage("ReSharper", "PossibleInvalidOperationException")]
|
||||
private void ToModel(StoreBaseData restModel, StoreData model)
|
||||
{
|
||||
// Dereference on .Value is fine. Those values should be set by the template
|
||||
var blob = model.GetStoreBlob();
|
||||
model.StoreName = restModel.Name;
|
||||
model.StoreWebsite = restModel.Website;
|
||||
model.Archived = restModel.Archived;
|
||||
model.SpeedPolicy = restModel.SpeedPolicy;
|
||||
model.Archived = restModel.Archived.Value;
|
||||
model.SpeedPolicy = restModel.SpeedPolicy.Value;
|
||||
PaymentMethodId.TryParse(restModel.DefaultPaymentMethod, out var defaultPaymentMethod);
|
||||
model.SetDefaultPaymentId(defaultPaymentMethod);
|
||||
//we do not include the default payment method in this model and instead opt to set it in the stores/storeid/payment-methods endpoints
|
||||
//blob
|
||||
@@ -237,31 +257,31 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
//we do not include ExcludedPaymentMethods in this model and instead opt to set it in stores/storeid/payment-methods endpoints
|
||||
//we do not include EmailSettings in this model and instead opt to set it in stores/storeid/email endpoints
|
||||
//we do not include OnChainMinValue and LightningMaxValue because moving the CurrencyValueJsonConverter to the Client csproj is hard and requires a refactor (#1571 & #1572)
|
||||
blob.NetworkFeeMode = restModel.NetworkFeeMode;
|
||||
blob.NetworkFeeMode = restModel.NetworkFeeMode.Value;
|
||||
blob.DefaultCurrency = restModel.DefaultCurrency;
|
||||
blob.ReceiptOptions = InvoiceDataBase.ReceiptOptions.Merge(restModel.Receipt, null);
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi;
|
||||
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints;
|
||||
blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback;
|
||||
blob.LazyPaymentMethods = restModel.LazyPaymentMethods;
|
||||
blob.RedirectAutomatically = restModel.RedirectAutomatically;
|
||||
blob.ShowRecommendedFee = restModel.ShowRecommendedFee;
|
||||
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget;
|
||||
blob.LightningAmountInSatoshi = restModel.LightningAmountInSatoshi.Value;
|
||||
blob.LightningPrivateRouteHints = restModel.LightningPrivateRouteHints.Value;
|
||||
blob.OnChainWithLnInvoiceFallback = restModel.OnChainWithLnInvoiceFallback.Value;
|
||||
blob.LazyPaymentMethods = restModel.LazyPaymentMethods.Value;
|
||||
blob.RedirectAutomatically = restModel.RedirectAutomatically.Value;
|
||||
blob.ShowRecommendedFee = restModel.ShowRecommendedFee.Value;
|
||||
blob.RecommendedFeeBlockTarget = restModel.RecommendedFeeBlockTarget.Value;
|
||||
blob.DefaultLang = restModel.DefaultLang;
|
||||
blob.StoreSupportUrl = restModel.SupportUrl;
|
||||
blob.MonitoringExpiration = restModel.MonitoringExpiration;
|
||||
blob.InvoiceExpiration = restModel.InvoiceExpiration;
|
||||
blob.DisplayExpirationTimer = restModel.DisplayExpirationTimer;
|
||||
blob.MonitoringExpiration = restModel.MonitoringExpiration.Value;
|
||||
blob.InvoiceExpiration = restModel.InvoiceExpiration.Value;
|
||||
blob.DisplayExpirationTimer = restModel.DisplayExpirationTimer.Value;
|
||||
blob.HtmlTitle = restModel.HtmlTitle;
|
||||
blob.AnyoneCanInvoice = restModel.AnyoneCanCreateInvoice;
|
||||
blob.AnyoneCanInvoice = restModel.AnyoneCanCreateInvoice.Value;
|
||||
blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate;
|
||||
blob.PaymentTolerance = restModel.PaymentTolerance;
|
||||
blob.PayJoinEnabled = restModel.PayJoinEnabled;
|
||||
blob.PaymentTolerance = restModel.PaymentTolerance.Value;
|
||||
blob.PayJoinEnabled = restModel.PayJoinEnabled.Value;
|
||||
blob.BrandColor = restModel.BrandColor;
|
||||
blob.ApplyBrandColorToBackend = restModel.ApplyBrandColorToBackend;
|
||||
blob.ApplyBrandColorToBackend = restModel.ApplyBrandColorToBackend.Value;
|
||||
blob.LogoUrl = restModel.LogoUrl is null ? null : UnresolvedUri.Create(restModel.LogoUrl);
|
||||
blob.CssUrl = restModel.CssUrl is null ? null : UnresolvedUri.Create(restModel.CssUrl);
|
||||
blob.RefundBOLT11Expiration = restModel.RefundBOLT11Expiration;
|
||||
blob.RefundBOLT11Expiration = restModel.RefundBOLT11Expiration.Value;
|
||||
blob.PaymentSoundUrl = restModel.PaymentSoundUrl is null ? null : UnresolvedUri.Create(restModel.PaymentSoundUrl);
|
||||
if (restModel.AutoDetectLanguage.HasValue)
|
||||
blob.AutoDetectLanguage = restModel.AutoDetectLanguage.Value;
|
||||
@@ -336,24 +356,25 @@ namespace BTCPayServer.Controllers.Greenfield
|
||||
{
|
||||
for (int index = 0; index < request.PaymentMethodCriteria.Count; index++)
|
||||
{
|
||||
var i = index;
|
||||
PaymentMethodCriteriaData pmc = request.PaymentMethodCriteria[index];
|
||||
if (string.IsNullOrEmpty(pmc.CurrencyCode))
|
||||
{
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[index].CurrencyCode, "CurrencyCode is required", this);
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[i].CurrencyCode, "CurrencyCode is required", this);
|
||||
}
|
||||
else if (_currencyNameTable.GetCurrencyData(pmc.CurrencyCode, false) is null)
|
||||
{
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[index].CurrencyCode, "CurrencyCode is invalid", this);
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[i].CurrencyCode, "CurrencyCode is invalid", this);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(pmc.PaymentMethodId) || PaymentMethodId.TryParse(pmc.PaymentMethodId) is null)
|
||||
{
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[index].PaymentMethodId, "Payment method was invalid", this);
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[i].PaymentMethodId, "Payment method was invalid", this);
|
||||
}
|
||||
|
||||
if (pmc.Amount < 0)
|
||||
{
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[index].Amount, "Amount must be greater than 0", this);
|
||||
request.AddModelError(data => data.PaymentMethodCriteria[i].Amount, "Amount must be greater than 0", this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +351,34 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
await UpdateViewBag();
|
||||
|
||||
if (command == "ResetTemplate")
|
||||
{
|
||||
ModelState.Clear();
|
||||
await _StoreRepository.SetDefaultStoreTemplate(null);
|
||||
this.TempData.SetStatusSuccess(StringLocalizer["Store template successfully unset"]);
|
||||
return RedirectToAction(nameof(Policies));
|
||||
}
|
||||
|
||||
if (command == "SetTemplate")
|
||||
{
|
||||
ModelState.Clear();
|
||||
var storeId = this.HttpContext.GetStoreData()?.Id;
|
||||
if (storeId is null)
|
||||
{
|
||||
this.TempData.SetStatusMessageModel(new()
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Error,
|
||||
Message = StringLocalizer["You need to select a store first"]
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
await _StoreRepository.SetDefaultStoreTemplate(storeId, GetUserId());
|
||||
this.TempData.SetStatusSuccess(StringLocalizer["Store template created from store '{0}'. New stores will inherit these settings.", HttpContext.GetStoreData().StoreName]);
|
||||
}
|
||||
return RedirectToAction(nameof(Policies));
|
||||
}
|
||||
|
||||
if (command == "add-domain")
|
||||
{
|
||||
ModelState.Clear();
|
||||
|
||||
@@ -72,8 +72,18 @@ public partial class UIStoresController
|
||||
{
|
||||
var isFallback = command is "scripting-toggle-fallback";
|
||||
var rateSettings = storeBlob.GetOrCreateRateSettings(isFallback);
|
||||
rateSettings.RateScripting = !rateSettings.RateScripting;
|
||||
rateSettings.RateScript = rateSettings.GetDefaultRateRules(_defaultRules, storeBlob.Spread).ToString();
|
||||
|
||||
if (!rateSettings.RateScripting)
|
||||
{
|
||||
rateSettings.RateScript = rateSettings.GetDefaultRateRules(_defaultRules, storeBlob.Spread).ToString();
|
||||
rateSettings.RateScripting = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
rateSettings.RateScripting = false;
|
||||
rateSettings.RateScript = null;
|
||||
}
|
||||
|
||||
CurrentStore.SetStoreBlob(storeBlob);
|
||||
await _storeRepo.UpdateStore(CurrentStore);
|
||||
if (rateSettings.RateScripting)
|
||||
@@ -179,7 +189,7 @@ public partial class UIStoresController
|
||||
}
|
||||
}
|
||||
|
||||
private void SetViewModel(RatesViewModel.Source vm, StoreBlob.RateSettings? rateSettings, StoreBlob storeBlob)
|
||||
private async Task SetViewModel(RatesViewModel.Source vm, StoreBlob.RateSettings? rateSettings, StoreBlob storeBlob)
|
||||
{
|
||||
if (rateSettings is null)
|
||||
return;
|
||||
@@ -191,7 +201,9 @@ public partial class UIStoresController
|
||||
vm.PreferredResolvedExchange = chosenSource.Id;
|
||||
vm.RateSource = chosenSource.Url;
|
||||
vm.Script = rateSettings.GetRateRules(_defaultRules, storeBlob.Spread).ToString();
|
||||
vm.DefaultScript = rateSettings.GetDefaultRateRules(_defaultRules, storeBlob.Spread).ToString();
|
||||
|
||||
var defaultRateSettings = (await _storeRepo.GetDefaultStoreTemplate()).GetStoreBlob()?.GetRateSettings(false) ?? new();
|
||||
vm.DefaultScript = defaultRateSettings.GetDefaultRateRules(_defaultRules, storeBlob.Spread).ToString();
|
||||
vm.ShowScripting = rateSettings.RateScripting;
|
||||
|
||||
vm.ScriptingConfirm = new()
|
||||
|
||||
@@ -24,7 +24,6 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
private readonly StoreRepository _repo;
|
||||
private readonly IStringLocalizer StringLocalizer;
|
||||
private readonly SettingsRepository _settingsRepository;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly DefaultRulesCollection _defaultRules;
|
||||
private readonly RateFetcher _rateFactory;
|
||||
@@ -35,15 +34,13 @@ namespace BTCPayServer.Controllers
|
||||
DefaultRulesCollection defaultRules,
|
||||
StoreRepository storeRepository,
|
||||
IStringLocalizer stringLocalizer,
|
||||
RateFetcher rateFactory,
|
||||
SettingsRepository settingsRepository)
|
||||
RateFetcher rateFactory)
|
||||
{
|
||||
_repo = storeRepository;
|
||||
StringLocalizer = stringLocalizer;
|
||||
_userManager = userManager;
|
||||
_defaultRules = defaultRules;
|
||||
_rateFactory = rateFactory;
|
||||
_settingsRepository = settingsRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -71,12 +68,16 @@ namespace BTCPayServer.Controllers
|
||||
public async Task<IActionResult> CreateStore(bool skipWizard)
|
||||
{
|
||||
var stores = await _repo.GetStoresByUserId(GetUserId());
|
||||
var defaultCurrency = (await _settingsRepository.GetSettingAsync<PoliciesSettings>())?.DefaultCurrency ?? StoreBlob.StandardDefaultCurrency;
|
||||
var defaultTemplate = await _repo.GetDefaultStoreTemplate();
|
||||
var blob = defaultTemplate.GetStoreBlob();
|
||||
var vm = new CreateStoreViewModel
|
||||
{
|
||||
Name = defaultTemplate.StoreName,
|
||||
IsFirstStore = !(stores.Any() || skipWizard),
|
||||
DefaultCurrency = defaultCurrency,
|
||||
Exchanges = GetExchangesSelectList(defaultCurrency, null)
|
||||
DefaultCurrency = blob.DefaultCurrency,
|
||||
Exchanges = GetExchangesSelectList(blob.DefaultCurrency, null),
|
||||
CanEditPreferredExchange = blob.GetRateSettings(false)?.RateScripting is not true,
|
||||
PreferredExchange = blob.GetRateSettings(false)?.PreferredExchange
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
@@ -90,12 +91,14 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var stores = await _repo.GetStoresByUserId(GetUserId());
|
||||
vm.IsFirstStore = !stores.Any();
|
||||
var defaultCurrency = (await _settingsRepository.GetSettingAsync<PoliciesSettings>())?.DefaultCurrency ?? StoreBlob.StandardDefaultCurrency;
|
||||
var template = await _repo.GetDefaultStoreTemplate();
|
||||
var defaultCurrency = template.GetStoreBlob().DefaultCurrency ?? StoreBlob.StandardDefaultCurrency;
|
||||
vm.Exchanges = GetExchangesSelectList(defaultCurrency, null);
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
var store = new StoreData { StoreName = vm.Name };
|
||||
var store = await _repo.GetDefaultStoreTemplate();
|
||||
store.StoreName = vm.Name;
|
||||
var blob = store.GetStoreBlob();
|
||||
blob.DefaultCurrency = vm.DefaultCurrency;
|
||||
blob.GetOrCreateRateSettings(false).PreferredExchange = vm.PreferredExchange;
|
||||
|
||||
@@ -45,8 +45,6 @@ namespace BTCPayServer.Data
|
||||
{
|
||||
storeData.DefaultCrypto = defaultPaymentId?.ToString();
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
|
||||
|
||||
public static StoreBlob GetStoreBlob(this StoreData storeData)
|
||||
{
|
||||
@@ -57,7 +55,7 @@ namespace BTCPayServer.Data
|
||||
result.PaymentMethodCriteria.RemoveAll(criteria => criteria?.PaymentMethod is null);
|
||||
return result;
|
||||
}
|
||||
|
||||
#pragma warning restore CS0618
|
||||
public static bool AnyPaymentMethodAvailable(this StoreData storeData, PaymentMethodHandlerDictionary handlers)
|
||||
{
|
||||
return storeData.GetPaymentMethodConfigs(handlers, true).Any();
|
||||
|
||||
@@ -22,5 +22,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public SelectList Exchanges { get; set; }
|
||||
public bool CanEditPreferredExchange { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,23 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
public class Source
|
||||
{
|
||||
public bool ShowScripting { get; set; }
|
||||
|
||||
[Display(Name = "Rate Rules")]
|
||||
[MaxLength(2000)]
|
||||
public string Script { get; set; }
|
||||
|
||||
public string DefaultScript { get; set; }
|
||||
|
||||
[Display(Name = "Preferred Price Source")]
|
||||
public string PreferredExchange { get; set; }
|
||||
|
||||
public SelectList Exchanges { get; set; }
|
||||
public string RateSource { get; set; }
|
||||
public string PreferredResolvedExchange { get; set; }
|
||||
public bool IsFallback { get; set; }
|
||||
public ConfirmModel ScriptingConfirm { get; set; }
|
||||
}
|
||||
|
||||
public class TestResultViewModel
|
||||
{
|
||||
public string CurrencyPair { get; set; }
|
||||
@@ -35,8 +40,9 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
|
||||
public Source PrimarySource { get; set; }
|
||||
public Source FallbackSource { get; set; }
|
||||
|
||||
[Display(Name = "Enable fallback rates")]
|
||||
public bool HasFallback {get; set; }
|
||||
public bool HasFallback { get; set; }
|
||||
|
||||
public string ScriptTest { get; set; }
|
||||
public string DefaultCurrencyPairs { get; set; }
|
||||
@@ -45,6 +51,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
||||
[Display(Name = "Add Exchange Rate Spread")]
|
||||
[Range(0.0, 100.0)]
|
||||
public double Spread { get; set; }
|
||||
|
||||
public IEnumerable<RateSourceInfo> AvailableExchanges { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.JsonConverters;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Validation;
|
||||
@@ -82,8 +83,6 @@ namespace BTCPayServer.Services
|
||||
|
||||
[Display(Name = "Show plugins in pre-release")]
|
||||
public bool PluginPreReleases { get; set; }
|
||||
[Display(Name = "Select the Default Currency during Store Creation")]
|
||||
public string DefaultCurrency { get; set; }
|
||||
|
||||
public bool DisableSSHService { get; set; }
|
||||
|
||||
@@ -101,6 +100,9 @@ namespace BTCPayServer.Services
|
||||
[Display(Name = "Default role for users on a new store")]
|
||||
public string DefaultRole { get; set; }
|
||||
|
||||
[Display(Name = "Default store template")]
|
||||
public JObject DefaultStoreTemplate { get; set; }
|
||||
|
||||
public class BlockExplorerOverrideItem
|
||||
{
|
||||
[JsonConverter(typeof(PaymentMethodIdJsonConverter))]
|
||||
|
||||
@@ -5,14 +5,21 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Contracts;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Migrations;
|
||||
using BTCPayServer.Payments;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NBitcoin;
|
||||
using NBitcoin.DataEncoders;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
using StoreWebhookData = BTCPayServer.Data.StoreWebhookData;
|
||||
using WebhookDeliveryData = BTCPayServer.Data.WebhookDeliveryData;
|
||||
|
||||
namespace BTCPayServer.Services.Stores
|
||||
{
|
||||
@@ -695,6 +702,67 @@ retry:
|
||||
LIMIT 1;
|
||||
""", new { storeId })) is true;
|
||||
}
|
||||
|
||||
public async Task<StoreData> GetDefaultStoreTemplate()
|
||||
{
|
||||
var data = new StoreData();
|
||||
var policies = await this._settingsRepository.GetSettingAsync<PoliciesSettings>();
|
||||
if (policies?.DefaultStoreTemplate is null)
|
||||
return data;
|
||||
var serializer = new NBXplorer.Serializer(null);
|
||||
serializer.Settings.DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate;
|
||||
var r = serializer.ToObject<RestrictedStoreData>(policies.DefaultStoreTemplate);
|
||||
if (!string.IsNullOrWhiteSpace(r.StoreName))
|
||||
data.StoreName = r.StoreName;
|
||||
if (r.SpeedPolicy is not null)
|
||||
data.SpeedPolicy = r.SpeedPolicy.Value;
|
||||
if (!string.IsNullOrWhiteSpace(r.StoreWebsite))
|
||||
data.StoreWebsite = r.StoreWebsite;
|
||||
if (!string.IsNullOrWhiteSpace(r.DefaultPaymentMethodId) && PaymentMethodId.TryParse(r.DefaultPaymentMethodId, out var paymentMethodId))
|
||||
data.SetDefaultPaymentId(paymentMethodId);
|
||||
if (r?.Blob is not null)
|
||||
data.SetStoreBlob(r.Blob);
|
||||
return data;
|
||||
}
|
||||
public async Task SetDefaultStoreTemplate(string storeId, string userId)
|
||||
{
|
||||
var storeData = await this.FindStore(storeId, userId);
|
||||
if (storeData is null)
|
||||
throw new InvalidOperationException("Store not found, or incorrect permissions");
|
||||
await SetDefaultStoreTemplate(storeData);
|
||||
}
|
||||
public async Task SetDefaultStoreTemplate(StoreData? store)
|
||||
{
|
||||
var policies = await this._settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new();
|
||||
if (store is null)
|
||||
{
|
||||
policies.DefaultStoreTemplate = null;
|
||||
await _settingsRepository.UpdateSetting(policies);
|
||||
return;
|
||||
}
|
||||
var serializer = new NBXplorer.Serializer(null);
|
||||
serializer.Settings.DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate;
|
||||
var r = new RestrictedStoreData()
|
||||
{
|
||||
StoreName = store.StoreName,
|
||||
SpeedPolicy = store.SpeedPolicy,
|
||||
StoreWebsite = store.StoreWebsite,
|
||||
DefaultPaymentMethodId = store.GetDefaultPaymentId()?.ToString(),
|
||||
Blob = store.GetStoreBlob()
|
||||
};
|
||||
policies.DefaultStoreTemplate = JObject.Parse(serializer.ToString(r));
|
||||
await _settingsRepository.UpdateSetting(policies);
|
||||
}
|
||||
|
||||
class RestrictedStoreData
|
||||
{
|
||||
public string? StoreName { get; set; }
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public SpeedPolicy? SpeedPolicy { get; set; }
|
||||
public string? StoreWebsite { get; set; }
|
||||
public string? DefaultPaymentMethodId { get; set; }
|
||||
public StoreBlob? Blob { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public record StoreRoleId
|
||||
|
||||
@@ -202,8 +202,23 @@
|
||||
|
||||
<h4 class="mt-5" text-translate="true">Customization</h4>
|
||||
<div class="form-group mb-3">
|
||||
<label asp-for="DefaultCurrency" class="form-label"></label>
|
||||
<input asp-for="DefaultCurrency" placeholder="@StoreBlob.StandardDefaultCurrency" class="form-control" currency-selection />
|
||||
<label asp-for="DefaultStoreTemplate" class="form-label"></label>
|
||||
@if (Model.DefaultStoreTemplate is null)
|
||||
{
|
||||
<div class="input-group">
|
||||
<input value="@StringLocalizer["Not configured"]" type="text" readonly class="form-control"/>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="SetTemplate" id="SetTemplate" text-translate="true">Create template from selected store</button>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="input-group">
|
||||
<input value="@StringLocalizer["Configured"]" type="text" readonly class="form-control"/>
|
||||
<button type="submit" class="btn btn-primary" name="command" value="SetTemplate" id="SetTemplate" text-translate="true">Replace template from selected store</button>
|
||||
<button type="submit" class="btn btn-danger" name="command" value="ResetTemplate" id="ResetTemplate" text-translate="true">Remove store template</button>
|
||||
</div>
|
||||
}
|
||||
<div class="form-text" text-translate="true">The store template sets defaults for all new stores. It includes rates settings, default invoice settings, checkout display settings but excludes sensitive data like access tokens, payment method settings, webhooks.</div>
|
||||
</div>
|
||||
<div class="form-group mb-5">
|
||||
<label asp-for="RootAppId" class="form-label"></label>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@model BTCPayServer.Models.StoreViewModels.CreateStoreViewModel
|
||||
|
||||
<form asp-action="CreateStore">
|
||||
<input type="hidden" asp-for="CanEditPreferredExchange"/>
|
||||
@if (!ViewContext.ModelState.IsValid)
|
||||
{
|
||||
<div asp-validation-summary="ModelOnly"></div>
|
||||
@@ -15,13 +16,16 @@
|
||||
<input asp-for="DefaultCurrency" class="form-control w-300px" currency-selection />
|
||||
<span asp-validation-for="DefaultCurrency" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange" class="form-label" data-required></label>
|
||||
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-select w-300px"></select>
|
||||
<div class="form-text mt-2 only-for-js" text-translate="true">The recommended price source gets chosen based on the default currency.</div>
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
</div>
|
||||
@if (Model.CanEditPreferredExchange)
|
||||
{
|
||||
<div class="form-group">
|
||||
<label asp-for="PreferredExchange" class="form-label" data-required></label>
|
||||
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-select w-300px"></select>
|
||||
<div class="form-text mt-2 only-for-js" text-translate="true">The recommended price source gets chosen based on the default currency.</div>
|
||||
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
<div class="form-group mt-4">
|
||||
<input type="submit" value="Create Store" class="btn btn-primary @(Model.IsFirstStore ? "w-100" : null)" id="Create" />
|
||||
<input type="submit" value="Create Store" class="btn btn-primary @(Model.IsFirstStore ? "w-100" : null)" id="Create" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"Stores"
|
||||
],
|
||||
"summary": "Create a new store",
|
||||
"description": "Create a new store",
|
||||
"description": "Create a new store (default values can be different if the server settings use a default store template)",
|
||||
"requestBody": {
|
||||
"x-name": "request",
|
||||
"content": {
|
||||
@@ -143,7 +143,7 @@
|
||||
"$ref": "#/components/parameters/StoreId"
|
||||
}
|
||||
],
|
||||
"description": "Update the specified store",
|
||||
"description": "Update the specified store (default values can be different if the server settings use a default store template)",
|
||||
"requestBody": {
|
||||
"x-name": "request",
|
||||
"content": {
|
||||
@@ -541,7 +541,7 @@
|
||||
]
|
||||
},
|
||||
"monitoringExpiration": {
|
||||
"default": 3600,
|
||||
"default": 86400,
|
||||
"minimum": 600,
|
||||
"maximum": 2073600,
|
||||
"description": "The time after which an invoice which has been paid but not confirmed will be considered invalid. The value will be rounded down to a minute.",
|
||||
|
||||
Reference in New Issue
Block a user