Branding updates for 2.0 (#5947)

* Remove deprecated CSS options

Closes #5945.

* Greenfield: Add brandColor to store APIs

Closes #5946.

* Migrate file IDs to URLs

Closes #5953.

* Greenfield: Add CSS and logo URL to store settings API

Closes #5945.

* Add migration test

* Store and Server branding can reference file's via fileid:ID

* Add PaymentSoundUrl to Store API

---------

Co-authored-by: nicolas.dorier <nicolas.dorier@gmail.com>
This commit is contained in:
d11n 2024-05-09 02:18:02 +02:00 committed by GitHub
parent eba3475a1b
commit 4c303d358b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
66 changed files with 526 additions and 605 deletions

View File

@ -35,13 +35,11 @@ namespace BTCPayServer.Client.Models
public string CustomAmountPayButtonText { get; set; } = null;
public string FixedAmountPayButtonText { get; set; } = null;
public string TipText { get; set; } = null;
public string CustomCSSLink { get; set; } = null;
public string NotificationUrl { get; set; } = null;
public string RedirectUrl { get; set; } = null;
public bool? RedirectAutomatically { get; set; } = null;
public bool? Archived { get; set; } = null;
public string FormId { get; set; } = null;
public string EmbeddedCSS { get; set; } = null;
}
public enum CrowdfundResetEvery
@ -65,9 +63,7 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? EndDate { get; set; } = null;
public decimal? TargetAmount { get; set; } = null;
public string CustomCSSLink { get; set; } = null;
public string MainImageUrl { get; set; } = null;
public string EmbeddedCSS { get; set; } = null;
public string NotificationUrl { get; set; } = null;
public string Tagline { get; set; } = null;
public string PerksTemplate { get; set; } = null;

View File

@ -17,9 +17,6 @@ namespace BTCPayServer.Client.Models
public string Title { get; set; }
public string Description { get; set; }
public string Email { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
public bool AllowCustomPaymentAmounts { get; set; }
[JsonExtensionData]

View File

@ -30,11 +30,9 @@ namespace BTCPayServer.Client.Models
public string FixedAmountPayButtonText { get; set; }
public string CustomAmountPayButtonText { get; set; }
public string TipText { get; set; }
public string CustomCSSLink { get; set; }
public string NotificationUrl { get; set; }
public string RedirectUrl { get; set; }
public string Description { get; set; }
public string EmbeddedCSS { get; set; }
public bool? RedirectAutomatically { get; set; }
}
@ -50,9 +48,7 @@ namespace BTCPayServer.Client.Models
[JsonConverter(typeof(NBitcoin.JsonConverters.DateTimeToUnixTimeConverter))]
public DateTimeOffset? EndDate { get; set; }
public decimal? TargetAmount { get; set; }
public string CustomCSSLink { get; set; }
public string MainImageUrl { get; set; }
public string EmbeddedCSS { get; set; }
public string NotificationUrl { get; set; }
public string Tagline { get; set; }
public object Perks { get; set; }

View File

@ -16,6 +16,11 @@ namespace BTCPayServer.Client.Models
public string Website { get; set; }
public string BrandColor { get; set; }
public string LogoUrl { get; set; }
public string CssUrl { get; set; }
public string PaymentSoundUrl { get; set; }
public string SupportUrl { get; set; }
[JsonConverter(typeof(TimeSpanJsonConverter.Seconds))]

View File

@ -0,0 +1,61 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240508015052_fileid")]
public partial class fileid : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("""
UPDATE "Settings"
SET "Value" = jsonb_set(
"Value",
'{LogoUrl}',
to_jsonb('fileid:' || ("Value"->>'LogoFileId'))) - 'LogoFileId'
WHERE "Id" = 'BTCPayServer.Services.ThemeSettings'
AND "Value"->>'LogoFileId' IS NOT NULL;
UPDATE "Settings"
SET "Value" = jsonb_set(
"Value",
'{CustomThemeCssUrl}',
to_jsonb('fileid:' || ("Value"->>'CustomThemeFileId'))) - 'CustomThemeFileId'
WHERE "Id" = 'BTCPayServer.Services.ThemeSettings'
AND "Value"->>'CustomThemeFileId' IS NOT NULL;
UPDATE "Stores"
SET "StoreBlob" = jsonb_set(
"StoreBlob",
'{logoUrl}',
to_jsonb('fileid:' || ("StoreBlob"->>'logoFileId'))) - 'logoFileId'
WHERE "StoreBlob"->>'logoFileId' IS NOT NULL;
UPDATE "Stores"
SET "StoreBlob" = jsonb_set(
"StoreBlob",
'{cssUrl}',
to_jsonb('fileid:' || ("StoreBlob"->>'cssFileId'))) - 'cssFileId'
WHERE "StoreBlob"->>'cssFileId' IS NOT NULL;
UPDATE "Stores"
SET "StoreBlob" = jsonb_set(
"StoreBlob",
'{paymentSoundUrl}',
to_jsonb('fileid:' || ("StoreBlob"->>'soundFileId'))) - 'soundFileId'
WHERE "StoreBlob"->>'soundFileId' IS NOT NULL;
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -64,6 +64,11 @@ namespace BTCPayServer.Tests
get;
set;
}
public Uri ServerUriWithIP
{
get;
set;
}
public string MySQL
{
@ -164,6 +169,7 @@ namespace BTCPayServer.Tests
await File.WriteAllTextAsync(confPath, config.ToString());
ServerUri = new Uri("http://" + HostName + ":" + Port + "/");
ServerUriWithIP = new Uri("http://127.0.0.1:" + Port + "/");
HttpClient = new HttpClient();
HttpClient.BaseAddress = ServerUri;
Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");

View File

@ -1365,6 +1365,15 @@ namespace BTCPayServer.Tests
//create store
var newStore = await client.CreateStore(new CreateStoreRequest { Name = "A" });
Assert.Equal("A", newStore.Name);
// validate
await AssertValidationError(["CssUrl", "LogoUrl", "BrandColor"], async () =>
await client.UpdateStore(newStore.Id, new UpdateStoreRequest
{
CssUrl = "style.css",
LogoUrl = "logo.svg",
BrandColor = "invalid"
}));
//update store
Assert.Empty(newStore.PaymentMethodCriteria);
@ -1372,6 +1381,9 @@ namespace BTCPayServer.Tests
var updatedStore = await client.UpdateStore(newStore.Id, new UpdateStoreRequest
{
Name = "B",
CssUrl = "https://example.org/style.css",
LogoUrl = "https://example.org/logo.svg",
BrandColor = "#003366",
PaymentMethodCriteria = new List<PaymentMethodCriteriaData>
{
new()
@ -1384,6 +1396,9 @@ namespace BTCPayServer.Tests
}
});
Assert.Equal("B", updatedStore.Name);
Assert.Equal("https://example.org/style.css", updatedStore.CssUrl);
Assert.Equal("https://example.org/logo.svg", updatedStore.LogoUrl);
Assert.Equal("#003366", updatedStore.BrandColor);
var s = (await client.GetStore(newStore.Id));
Assert.Equal("B", s.Name);
var pmc = Assert.Single(s.PaymentMethodCriteria);

View File

@ -146,7 +146,7 @@ namespace BTCPayServer.Tests
public async Task ModifyPayment(Action<GeneralSettingsViewModel> modify)
{
var storeController = GetController<UIStoresController>();
var response = storeController.GeneralSettings();
var response = await storeController.GeneralSettings();
GeneralSettingsViewModel settings = (GeneralSettingsViewModel)((ViewResult)response).Model;
modify(settings);
await storeController.GeneralSettings(settings);

View File

@ -57,7 +57,6 @@ namespace BTCPayServer.Tests
Assert.IsType<ViewResult>(
await controller.EditAzureBlobStorageStorageProvider(azureBlobStorageConfiguration));
var shouldBeRedirectingToAzureStorageConfigPage =
Assert.IsType<RedirectToActionResult>(await controller.Storage());
Assert.Equal(nameof(StorageProvider), shouldBeRedirectingToAzureStorageConfigPage.ActionName);
@ -72,9 +71,8 @@ namespace BTCPayServer.Tests
await controller.StorageProvider(StorageProvider.AzureBlobStorage.ToString()))
.Model).ConnectionString);
await UnitTest1.CanUploadRemoveFiles(controller);
var fileId = await UnitTest1.CanUploadFile(controller);
await UnitTest1.CanRemoveFile(controller, fileId);
}
[Fact(Skip = "Fail on CI")]

View File

@ -1,4 +1,5 @@
using System;
using Dapper;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -53,7 +54,6 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using BTCPayServer.Storage.ViewModels;
using ExchangeSharp;
using Fido2NetLib;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -79,6 +79,7 @@ using CreatePaymentRequestRequest = BTCPayServer.Client.Models.CreatePaymentRequ
using MarkPayoutRequest = BTCPayServer.Client.Models.MarkPayoutRequest;
using PaymentRequestData = BTCPayServer.Client.Models.PaymentRequestData;
using RatesViewModel = BTCPayServer.Models.StoreViewModels.RatesViewModel;
using Microsoft.Extensions.Caching.Memory;
namespace BTCPayServer.Tests
{
@ -297,7 +298,7 @@ namespace BTCPayServer.Tests
// Set tolerance to 50%
var stores = user.GetController<UIStoresController>();
var response = stores.GeneralSettings();
var response = await stores.GeneralSettings();
var vm = Assert.IsType<GeneralSettingsViewModel>(Assert.IsType<ViewResult>(response).Model);
Assert.Equal(0.0, vm.PaymentTolerance);
vm.PaymentTolerance = 50.0;
@ -439,7 +440,7 @@ namespace BTCPayServer.Tests
var user = tester.NewAccount();
await user.GrantAccessAsync(true);
var storeController = user.GetController<UIStoresController>();
var storeResponse = storeController.GeneralSettings();
var storeResponse = await storeController.GeneralSettings();
Assert.IsType<ViewResult>(storeResponse);
Assert.IsType<ViewResult>(storeController.SetupLightningNode(user.StoreId, "BTC"));
@ -838,7 +839,7 @@ namespace BTCPayServer.Tests
var time = invoice.InvoiceTime;
AssertSearchInvoice(acc, true, invoice.Id, $"startdate:{time.ToString("yyyy-MM-dd HH:mm:ss")}");
AssertSearchInvoice(acc, true, invoice.Id, $"enddate:{time.ToStringLowerInvariant()}");
AssertSearchInvoice(acc, true, invoice.Id, $"enddate:{time.ToString().ToLowerInvariant()}");
AssertSearchInvoice(acc, false, invoice.Id,
$"startdate:{time.AddSeconds(1).ToString("yyyy-MM-dd HH:mm:ss")}");
AssertSearchInvoice(acc, false, invoice.Id,
@ -1499,8 +1500,7 @@ namespace BTCPayServer.Tests
var btcMethod = PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString();
// We allow BTC and LN, but not BTC under 5 USD, so only LN should be in the invoice
var vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().CheckoutAppearance()).Model);
var vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
Assert.Equal(2, vm.PaymentMethodCriteria.Count);
var criteria = Assert.Single(vm.PaymentMethodCriteria.Where(m => m.PaymentMethod == btcMethod.ToString()));
Assert.Equal(PaymentTypes.CHAIN.GetPaymentMethodId("BTC").ToString(), criteria.PaymentMethod);
@ -1527,8 +1527,7 @@ namespace BTCPayServer.Tests
// Let's replicate https://github.com/btcpayserver/btcpayserver/issues/2963
// We allow BTC for more than 5 USD, and LN for less than 150. The default is LN, so the default
// payment method should be LN.
vm = Assert.IsType<CheckoutAppearanceViewModel>(Assert
.IsType<ViewResult>(user.GetController<UIStoresController>().CheckoutAppearance()).Model);
vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
vm.DefaultPaymentMethod = lnMethod;
criteria = vm.PaymentMethodCriteria.First();
criteria.Value = "150 USD";
@ -1640,7 +1639,7 @@ namespace BTCPayServer.Tests
user.GrantAccess(true);
user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, false);
var vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
var vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
var criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode).ToString(), criteria.PaymentMethod);
criteria.Value = "2 USD";
@ -1660,7 +1659,7 @@ namespace BTCPayServer.Tests
// Activating LNUrl, we should still have only 1 payment criteria that can be set.
user.RegisterLightningNode(cryptoCode);
user.SetLNUrl(cryptoCode, true);
vm = user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModel<CheckoutAppearanceViewModel>();
vm = await user.GetController<UIStoresController>().CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
criteria = Assert.Single(vm.PaymentMethodCriteria);
Assert.Equal(PaymentTypes.LN.GetPaymentMethodId(cryptoCode).ToString(), criteria.PaymentMethod);
Assert.IsType<RedirectToActionResult>(user.GetController<UIStoresController>().CheckoutAppearance(vm).Result);
@ -2507,6 +2506,90 @@ namespace BTCPayServer.Tests
}
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanMigrateFileIds()
{
using var tester = CreateServerTester(newDb: true);
tester.DeleteStore = false;
await tester.StartAsync();
var user = tester.NewAccount();
await user.GrantAccessAsync();
using (var ctx = tester.PayTester.GetService<ApplicationDbContextFactory>().CreateContext())
{
var storeConfig = """
{
"spread": 0.0,
"cssFileId": "2a51c49a-9d54-4013-80a2-3f6e69d08523",
"logoFileId": "8f890691-87f9-4c65-80e5-3b7ffaa3551f",
"soundFileId": "62bc4757-b92b-4a3b-a8ab-0e9b693d6a29",
"networkFeeMode": "MultiplePaymentsOnly",
"defaultCurrency": "USD",
"showStoreHeader": true,
"celebratePayment": true,
"paymentTolerance": 0.0,
"invoiceExpiration": 15,
"preferredExchange": "kraken",
"showRecommendedFee": true,
"monitoringExpiration": 1440,
"showPayInWalletButton": true,
"displayExpirationTimer": 5,
"excludedPaymentMethods": null,
"recommendedFeeBlockTarget": 1
}
""";
var serverConfig = """
{
"CssUri": null,
"FirstRun": false,
"LogoFileId": "ce71d90a-dd90-40a3-b1f0-96d00c9abb52",
"CustomTheme": true,
"CustomThemeCssUri": null,
"CustomThemeFileId": "9b00f4ed-914b-437b-abd2-9a90c1b22c34",
"CustomThemeExtension": 0
}
""";
await ctx.Database.GetDbConnection().ExecuteAsync("""
UPDATE "Stores" SET "StoreBlob"=@storeConfig::JSONB WHERE "Id"=@storeId;
""", new { storeId = user.StoreId, storeConfig });
await ctx.Database.GetDbConnection().ExecuteAsync("""
UPDATE "Settings" SET "Value"=@serverConfig::JSONB WHERE "Id"='BTCPayServer.Services.ThemeSettings';
""", new { serverConfig });
await ctx.Database.GetDbConnection().ExecuteAsync("""
INSERT INTO "Files" VALUES (@id, @fileName, @id || '-' || @fileName, NOW(), @userId);
""",
new[]
{
new { id = "2a51c49a-9d54-4013-80a2-3f6e69d08523", fileName = "store.css", userId = user.UserId },
new { id = "8f890691-87f9-4c65-80e5-3b7ffaa3551f", fileName = "store.png", userId = user.UserId },
new { id = "ce71d90a-dd90-40a3-b1f0-96d00c9abb52", fileName = "admin.png", userId = user.UserId },
new { id = "9b00f4ed-914b-437b-abd2-9a90c1b22c34", fileName = "admin.css", userId = user.UserId },
new { id = "62bc4757-b92b-4a3b-a8ab-0e9b693d6a29", fileName = "store.mp3", userId = user.UserId },
});
await ctx.Database.GetDbConnection().ExecuteAsync("""
DELETE FROM "__EFMigrationsHistory" WHERE "MigrationId"='20240508015052_fileid'
""");
await ctx.Database.MigrateAsync();
((MemoryCache)tester.PayTester.GetService<IMemoryCache>()).Clear();
}
var controller = tester.PayTester.GetController<UIStoresController>(user.UserId, user.StoreId);
var vm = await controller.GeneralSettings().AssertViewModelAsync<GeneralSettingsViewModel>();
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/8f890691-87f9-4c65-80e5-3b7ffaa3551f-store.png", vm.LogoUrl);
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/2a51c49a-9d54-4013-80a2-3f6e69d08523-store.css", vm.CssUrl);
var vm2 = await controller.CheckoutAppearance().AssertViewModelAsync<CheckoutAppearanceViewModel>();
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/62bc4757-b92b-4a3b-a8ab-0e9b693d6a29-store.mp3", vm2.PaymentSoundUrl);
var serverController = tester.PayTester.GetController<UIServerController>();
var branding = await serverController.Branding().AssertViewModelAsync<BrandingViewModel>();
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/ce71d90a-dd90-40a3-b1f0-96d00c9abb52-admin.png", branding.LogoUrl);
Assert.Equal(tester.PayTester.ServerUriWithIP + "LocalStorage/9b00f4ed-914b-437b-abd2-9a90c1b22c34-admin.css", branding.CustomThemeCssUrl);
}
[Fact(Timeout = LongRunningTestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanDoLightningInternalNodeMigration()
@ -2943,14 +3026,14 @@ namespace BTCPayServer.Tests
Assert.Equal(StorageProvider.FileSystem,
shouldBeRedirectingToLocalStorageConfigPage.RouteValues["provider"]);
await CanUploadRemoveFiles(controller);
var fileId = await CanUploadFile(controller);
await CanRemoveFile(controller, fileId);
}
internal static async Task CanUploadRemoveFiles(UIServerController controller)
internal static async Task<string> CanUploadFile(UIServerController controller)
{
var fileContent = "content";
List<IFormFile> fileList = new List<IFormFile>();
fileList.Add(TestUtils.GetFormFile("uploadtestfile1.txt", fileContent));
var fileList = new List<IFormFile> { TestUtils.GetFormFile("uploadtestfile1.txt", fileContent) };
var uploadFormFileResult = Assert.IsType<RedirectToActionResult>(await controller.CreateFiles(fileList));
Assert.True(uploadFormFileResult.RouteValues.ContainsKey("fileIds"));
@ -2966,7 +3049,6 @@ namespace BTCPayServer.Tests
Assert.True(viewFilesViewModel.DirectUrlByFiles.ContainsKey(fileId));
Assert.NotEmpty(viewFilesViewModel.DirectUrlByFiles[fileId]);
//verify file is available and the same
using var net = new HttpClient();
var data = await net.GetStringAsync(new Uri(viewFilesViewModel.DirectUrlByFiles[fileId]));
@ -2991,17 +3073,20 @@ namespace BTCPayServer.Tests
data = await net.GetStringAsync(new Uri(url));
Assert.Equal(fileContent, data);
return fileId;
}
internal static async Task CanRemoveFile(UIServerController controller, string fileId)
{
//delete file
Assert.IsType<RedirectToActionResult>(await controller.DeleteFile(fileId));
statusMessageModel = controller.TempData.GetStatusMessageModel();
var statusMessageModel = controller.TempData.GetStatusMessageModel();
Assert.NotNull(statusMessageModel);
Assert.Equal(StatusMessageModel.StatusSeverity.Success, statusMessageModel.Severity);
//attempt to fetch deleted file
viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files(new string[] { fileId })).Model);
var viewFilesViewModel =
Assert.IsType<ViewFilesViewModel>(Assert.IsType<ViewResult>(await controller.Files([fileId])).Model);
Assert.Null(viewFilesViewModel.DirectUrlByFiles);
}

View File

@ -1,13 +1,12 @@
@using BTCPayServer.Services
@using BTCPayServer.Abstractions.Contracts
@inject ThemeSettings Theme
@inject IFileService FileService
@inject UriResolver UriResolver
@model BTCPayServer.Components.MainLogo.MainLogoViewModel
@if (!string.IsNullOrEmpty(Theme.LogoFileId))
@if (Theme.LogoUrl is not null)
{
var logoSrc = await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Theme.LogoFileId);
<img src="@logoSrc" alt="BTCPay Server" class="main-logo main-logo-custom @Model.CssClass" />
var logoUrl = await UriResolver.Resolve(this.Context.Request.GetAbsoluteRootUri(), Theme.LogoUrl);
<img src="@logoUrl" alt="BTCPay Server" class="main-logo main-logo-custom @Model.CssClass" />
}
else
{

View File

@ -1,11 +1,8 @@
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Client
@using BTCPayServer.Components.MainLogo
@using BTCPayServer.Services
@using BTCPayServer.Views.Server
@using BTCPayServer.Views.Stores
@inject BTCPayServerEnvironment Env
@inject IFileService FileService
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
@functions {
@* ReSharper disable once CSharpWarnings::CS1998 *@
@ -39,9 +36,9 @@ else
<div id="StoreSelector">
<div id="StoreSelectorDropdown" class="dropdown only-for-js">
<button id="StoreSelectorToggle" class="btn btn-secondary dropdown-toggle rounded-pill px-3 @(Model.CurrentStoreId == null ? "empty-state" : "")" type="button" data-bs-toggle="dropdown" aria-expanded="false">
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoFileId))
@if (!string.IsNullOrEmpty(Model.CurrentStoreLogoUrl))
{
<img class="logo" src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CurrentStoreLogoFileId))" alt="@Model.CurrentDisplayName" />
<img class="logo" src="@Model.CurrentStoreLogoUrl" alt="@Model.CurrentDisplayName" />
}
else
{

View File

@ -1,8 +1,11 @@
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Identity;
@ -14,13 +17,16 @@ namespace BTCPayServer.Components.StoreSelector
public class StoreSelector : ViewComponent
{
private readonly StoreRepository _storeRepo;
private readonly UriResolver _uriResolver;
private readonly UserManager<ApplicationUser> _userManager;
public StoreSelector(
StoreRepository storeRepo,
UriResolver uriResolver,
UserManager<ApplicationUser> userManager)
{
_storeRepo = storeRepo;
_uriResolver = uriResolver;
_userManager = userManager;
}
@ -50,7 +56,7 @@ namespace BTCPayServer.Components.StoreSelector
Options = options,
CurrentStoreId = currentStore?.Id,
CurrentDisplayName = currentStore?.StoreName,
CurrentStoreLogoFileId = blob?.LogoFileId,
CurrentStoreLogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), blob?.LogoUrl),
ArchivedCount = archivedCount
};

View File

@ -7,7 +7,7 @@ namespace BTCPayServer.Components.StoreSelector
{
public List<StoreSelectorOption> Options { get; set; }
public string CurrentStoreId { get; set; }
public string CurrentStoreLogoFileId { get; set; }
public string CurrentStoreLogoUrl { get; set; }
public string CurrentDisplayName { get; set; }
public int ArchivedCount { get; set; }
}

View File

@ -233,9 +233,7 @@ namespace BTCPayServer.Controllers.Greenfield
Description = request.Description?.Trim(),
EndDate = request.EndDate?.UtcDateTime,
TargetAmount = request.TargetAmount,
CustomCSSLink = request.CustomCSSLink?.Trim(),
MainImageUrl = request.MainImageUrl?.Trim(),
EmbeddedCSS = request.EmbeddedCSS?.Trim(),
NotificationUrl = request.NotificationUrl?.Trim(),
Tagline = request.Tagline?.Trim(),
PerksTemplate = request.PerksTemplate is not null ? AppService.SerializeTemplate(AppService.Parse(request.PerksTemplate.Trim())) : null,
@ -272,11 +270,9 @@ namespace BTCPayServer.Controllers.Greenfield
ButtonText = request.FixedAmountPayButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = request.CustomAmountPayButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = request.TipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomCSSLink = request.CustomCSSLink,
NotificationUrl = request.NotificationUrl,
RedirectUrl = request.RedirectUrl,
Description = request.Description,
EmbeddedCSS = request.EmbeddedCSS,
RedirectAutomatically = request.RedirectAutomatically,
FormId = request.FormId
};
@ -341,11 +337,9 @@ namespace BTCPayServer.Controllers.Greenfield
FixedAmountPayButtonText = settings.ButtonText,
CustomAmountPayButtonText = settings.CustomButtonText,
TipText = settings.CustomTipText,
CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
RedirectUrl = settings.RedirectUrl,
Description = settings.Description,
EmbeddedCSS = settings.EmbeddedCSS,
RedirectAutomatically = settings.RedirectAutomatically ?? false,
};
}
@ -399,9 +393,7 @@ namespace BTCPayServer.Controllers.Greenfield
Description = settings.Description,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink,
MainImageUrl = settings.MainImageUrl,
EmbeddedCSS = settings.EmbeddedCSS,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline,
Perks = JsonConvert.DeserializeObject(

View File

@ -234,9 +234,6 @@ namespace BTCPayServer.Controllers.Greenfield
if (string.IsNullOrEmpty(data.Title))
ModelState.AddModelError(nameof(data.Title), "Title is required");
if (!string.IsNullOrEmpty(data.CustomCSSLink) && data.CustomCSSLink.Length > 500)
ModelState.AddModelError(nameof(data.CustomCSSLink), "CustomCSSLink is 500 chars max");
return !ModelState.IsValid ? this.CreateValidationError(ModelState) : null;
}
@ -257,8 +254,6 @@ namespace BTCPayServer.Controllers.Greenfield
ExpiryDate = blob.ExpiryDate,
Email = blob.Email,
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts,
EmbeddedCSS = blob.EmbeddedCSS,
CustomCSSLink = blob.CustomCSSLink,
FormResponse = blob.FormResponse,
FormId = blob.FormId
};

View File

@ -116,6 +116,10 @@ namespace BTCPayServer.Controllers.Greenfield
Name = data.StoreName,
Website = data.StoreWebsite,
Archived = data.Archived,
BrandColor = storeBlob.BrandColor,
CssUrl = storeBlob.CssUrl?.ToString(),
LogoUrl = storeBlob.LogoUrl?.ToString(),
PaymentSoundUrl = storeBlob.PaymentSoundUrl?.ToString(),
SupportUrl = storeBlob.StoreSupportUrl,
SpeedPolicy = data.SpeedPolicy,
DefaultPaymentMethod = data.GetDefaultPaymentId()?.ToString(),
@ -192,6 +196,10 @@ namespace BTCPayServer.Controllers.Greenfield
blob.LightningDescriptionTemplate = restModel.LightningDescriptionTemplate;
blob.PaymentTolerance = restModel.PaymentTolerance;
blob.PayJoinEnabled = restModel.PayJoinEnabled;
blob.BrandColor = restModel.BrandColor;
blob.LogoUrl = restModel.LogoUrl is null ? null : UnresolvedUri.Create(restModel.LogoUrl);
blob.CssUrl = restModel.CssUrl is null ? null : UnresolvedUri.Create(restModel.CssUrl);
blob.PaymentSoundUrl = restModel.PaymentSoundUrl is null ? null : UnresolvedUri.Create(restModel.PaymentSoundUrl);
if (restModel.AutoDetectLanguage.HasValue)
blob.AutoDetectLanguage = restModel.AutoDetectLanguage.Value;
if (restModel.ShowPayInWalletButton.HasValue)
@ -237,6 +245,18 @@ namespace BTCPayServer.Controllers.Greenfield
{
ModelState.AddModelError(nameof(request.Website), "Website is not a valid url");
}
if (!string.IsNullOrEmpty(request.LogoUrl) && !Uri.TryCreate(request.LogoUrl, UriKind.Absolute, out _))
{
ModelState.AddModelError(nameof(request.LogoUrl), "Logo is not a valid url");
}
if (!string.IsNullOrEmpty(request.CssUrl) && !Uri.TryCreate(request.CssUrl, UriKind.Absolute, out _))
{
ModelState.AddModelError(nameof(request.CssUrl), "CSS is not a valid url");
}
if (!string.IsNullOrEmpty(request.BrandColor) && !ColorPalette.IsValid(request.BrandColor))
{
ModelState.AddModelError(nameof(request.BrandColor), "Brand color is not a valid HEX Color (e.g. #F7931A)");
}
if (request.InvoiceExpiration < TimeSpan.FromMinutes(1) && request.InvoiceExpiration > TimeSpan.FromMinutes(60 * 24 * 24))
ModelState.AddModelError(nameof(request.InvoiceExpiration), "InvoiceExpiration can only be between 1 and 34560 mins");
if (request.DisplayExpirationTimer < TimeSpan.FromMinutes(1) && request.DisplayExpirationTimer > TimeSpan.FromMinutes(60 * 24 * 24))

View File

@ -219,7 +219,7 @@ namespace BTCPayServer.Controllers
Currency = i.Currency,
Timestamp = i.InvoiceTime,
StoreName = store.StoreName,
StoreBranding = new StoreBrandingViewModel(storeBlob),
StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob),
ReceiptOptions = receipt
};
@ -889,7 +889,7 @@ namespace BTCPayServer.Controllers
DefaultLang = lang ?? invoice.DefaultLanguage ?? storeBlob.DefaultLang ?? "en",
ShowPayInWalletButton = storeBlob.ShowPayInWalletButton,
ShowStoreHeader = storeBlob.ShowStoreHeader,
StoreBranding = new StoreBrandingViewModel(storeBlob),
StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob),
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",
CelebratePayment = storeBlob.CelebratePayment,
OnChainWithLnInvoiceFallback = storeBlob.OnChainWithLnInvoiceFallback,
@ -978,9 +978,9 @@ namespace BTCPayServer.Controllers
if (storeBlob.PlaySoundOnPayment)
{
model.PaymentSoundUrl = string.IsNullOrEmpty(storeBlob.SoundFileId)
model.PaymentSoundUrl = storeBlob.PaymentSoundUrl is null
? string.Concat(Request.GetAbsoluteRootUri().ToString(), "checkout/payment.mp3")
: await _fileService.GetFileUrl(Request.GetAbsoluteRootUri(), storeBlob.SoundFileId);
: await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.PaymentSoundUrl);
model.ErrorSoundUrl = string.Concat(Request.GetAbsoluteRootUri().ToString(), "checkout/error.mp3");
model.NfcReadSoundUrl = string.Concat(Request.GetAbsoluteRootUri().ToString(), "checkout/nfcread.mp3");
}

View File

@ -65,6 +65,7 @@ namespace BTCPayServer.Controllers
private readonly PaymentMethodViewProvider _viewProvider;
private readonly AppService _appService;
private readonly IFileService _fileService;
private readonly UriResolver _uriResolver;
public WebhookSender WebhookNotificationManager { get; }
@ -91,6 +92,7 @@ namespace BTCPayServer.Controllers
LinkGenerator linkGenerator,
AppService appService,
IFileService fileService,
UriResolver uriResolver,
IAuthorizationService authorizationService,
TransactionLinkProviders transactionLinkProviders,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
@ -120,6 +122,7 @@ namespace BTCPayServer.Controllers
_paymentModelExtensions = paymentModelExtensions;
_viewProvider = viewProvider;
_fileService = fileService;
_uriResolver = uriResolver;
_appService = appService;
}

View File

@ -41,6 +41,7 @@ namespace BTCPayServer.Controllers
private readonly DisplayFormatter _displayFormatter;
private readonly InvoiceRepository _InvoiceRepository;
private readonly StoreRepository _storeRepository;
private readonly UriResolver _uriResolver;
private readonly BTCPayNetworkProvider _networkProvider;
private FormComponentProviders FormProviders { get; }
@ -55,6 +56,7 @@ namespace BTCPayServer.Controllers
CurrencyNameTable currencies,
DisplayFormatter displayFormatter,
StoreRepository storeRepository,
UriResolver uriResolver,
InvoiceRepository invoiceRepository,
FormComponentProviders formProviders,
FormDataService formDataService,
@ -68,6 +70,7 @@ namespace BTCPayServer.Controllers
_Currencies = currencies;
_displayFormatter = displayFormatter;
_storeRepository = storeRepository;
_uriResolver = uriResolver;
_InvoiceRepository = invoiceRepository;
FormProviders = formProviders;
FormDataService = formDataService;
@ -191,8 +194,6 @@ namespace BTCPayServer.Controllers
blob.Amount = viewModel.Amount;
blob.ExpiryDate = viewModel.ExpiryDate?.ToUniversalTime();
blob.Currency = viewModel.Currency ?? store.GetStoreBlob().DefaultCurrency;
blob.EmbeddedCSS = viewModel.EmbeddedCSS;
blob.CustomCSSLink = viewModel.CustomCSSLink;
blob.AllowCustomPaymentAmounts = viewModel.AllowCustomPaymentAmounts;
blob.FormId = viewModel.FormId;
@ -229,11 +230,7 @@ namespace BTCPayServer.Controllers
vm.HubPath = PaymentRequestHub.GetHubPath(Request);
vm.StoreName = store.StoreName;
vm.StoreWebsite = store.StoreWebsite;
vm.StoreBranding = new StoreBrandingViewModel(storeBlob)
{
EmbeddedCSS = vm.EmbeddedCSS,
CustomCSSLink = vm.CustomCSSLink
};
vm.StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob);
return View(vm);
}
@ -290,11 +287,8 @@ namespace BTCPayServer.Controllers
viewModel.Form = form;
var storeBlob = result.StoreData.GetStoreBlob();
viewModel.StoreBranding = new StoreBrandingViewModel(storeBlob)
{
EmbeddedCSS = prBlob.EmbeddedCSS,
CustomCSSLink = prBlob.CustomCSSLink
};
viewModel.StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob);
return View("Views/UIForms/View", viewModel);
}

View File

@ -8,6 +8,7 @@ using BTCPayServer.Lightning;
using BTCPayServer.Models;
using BTCPayServer.Payments;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
@ -21,16 +22,19 @@ namespace BTCPayServer.Controllers
{
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
private readonly Dictionary<PaymentMethodId, IPaymentModelExtension> _paymentModelExtensions;
private readonly UriResolver _uriResolver;
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly StoreRepository _StoreRepository;
public UIPublicLightningNodeInfoController(BTCPayNetworkProvider btcPayNetworkProvider,
Dictionary<PaymentMethodId, IPaymentModelExtension> paymentModelExtensions,
UriResolver uriResolver,
PaymentMethodHandlerDictionary handlers,
StoreRepository storeRepository)
{
_BtcPayNetworkProvider = btcPayNetworkProvider;
_paymentModelExtensions = paymentModelExtensions;
_uriResolver = uriResolver;
_handlers = handlers;
_StoreRepository = storeRepository;
}
@ -49,7 +53,7 @@ namespace BTCPayServer.Controllers
{
CryptoCode = cryptoCode,
StoreName = store.StoreName,
StoreBranding = new StoreBrandingViewModel(storeBlob)
StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob)
};
try
{

View File

@ -40,6 +40,7 @@ namespace BTCPayServer.Controllers
private readonly ApplicationDbContextFactory _dbContextFactory;
private readonly CurrencyNameTable _currencyNameTable;
private readonly DisplayFormatter _displayFormatter;
private readonly UriResolver _uriResolver;
private readonly PullPaymentHostedService _pullPaymentHostedService;
private readonly BTCPayNetworkProvider _networkProvider;
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings;
@ -51,6 +52,7 @@ namespace BTCPayServer.Controllers
public UIPullPaymentController(ApplicationDbContextFactory dbContextFactory,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
UriResolver uriResolver,
PullPaymentHostedService pullPaymentHostedService,
BTCPayNetworkProvider networkProvider,
BTCPayNetworkJsonSerializerSettings serializerSettings,
@ -62,6 +64,7 @@ namespace BTCPayServer.Controllers
_dbContextFactory = dbContextFactory;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
_uriResolver = uriResolver;
_pullPaymentHostedService = pullPaymentHostedService;
_serializerSettings = serializerSettings;
_payoutHandlers = payoutHandlers;
@ -121,11 +124,7 @@ namespace BTCPayServer.Controllers
}).ToList()
};
vm.IsPending &= vm.AmountDue > 0.0m;
vm.StoreBranding = new StoreBrandingViewModel(storeBlob)
{
EmbeddedCSS = blob.View.EmbeddedCSS,
CustomCSSLink = blob.View.CustomCSSLink
};
vm.StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob);
if (_pullPaymentHostedService.SupportsLNURL(blob))
{
@ -185,13 +184,11 @@ namespace BTCPayServer.Controllers
var blob = pp.GetBlob();
blob.Description = viewModel.Description ?? string.Empty;
blob.Name = viewModel.Name ?? string.Empty;
blob.View = new PullPaymentBlob.PullPaymentView()
blob.View = new PullPaymentBlob.PullPaymentView
{
Title = viewModel.Name ?? string.Empty,
Description = viewModel.Description ?? string.Empty,
CustomCSSLink = viewModel.CustomCSSLink,
Email = null,
EmbeddedCSS = viewModel.EmbeddedCSS,
Email = null
};
pp.SetBlob(blob);

View File

@ -65,6 +65,7 @@ namespace BTCPayServer.Controllers
private readonly IFileService _fileService;
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
private readonly LinkGenerator _linkGenerator;
private readonly UriResolver _uriResolver;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly TransactionLinkProviders _transactionLinkProviders;
@ -88,6 +89,7 @@ namespace BTCPayServer.Controllers
IOptions<ExternalServicesOptions> externalServiceOptions,
Logs logs,
LinkGenerator linkGenerator,
UriResolver uriResolver,
EmailSenderFactory emailSenderFactory,
IHostApplicationLifetime applicationLifetime,
IHtmlHelper html,
@ -113,6 +115,7 @@ namespace BTCPayServer.Controllers
_externalServiceOptions = externalServiceOptions;
Logs = logs;
_linkGenerator = linkGenerator;
_uriResolver = uriResolver;
_emailSenderFactory = emailSenderFactory;
ApplicationLifetime = applicationLifetime;
Html = html;
@ -1025,9 +1028,8 @@ namespace BTCPayServer.Controllers
ContactUrl = server.ContactUrl,
CustomTheme = theme.CustomTheme,
CustomThemeExtension = theme.CustomThemeExtension,
CustomThemeCssUri = theme.CustomThemeCssUri,
CustomThemeFileId = theme.CustomThemeFileId,
LogoFileId = theme.LogoFileId
CustomThemeCssUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), theme.CustomThemeCssUrl),
LogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), theme.LogoUrl)
};
return View(vm);
}
@ -1046,8 +1048,8 @@ namespace BTCPayServer.Controllers
if (userId is null)
return NotFound();
vm.LogoFileId = theme.LogoFileId;
vm.CustomThemeFileId = theme.CustomThemeFileId;
vm.LogoUrl = await _uriResolver.Resolve(this.Request.GetAbsoluteRootUri(), theme.LogoUrl);
vm.CustomThemeCssUrl = await _uriResolver.Resolve(this.Request.GetAbsoluteRootUri(), theme.CustomThemeCssUrl);
if (server.ServerName != vm.ServerName)
{
@ -1072,17 +1074,12 @@ namespace BTCPayServer.Controllers
{
if (vm.CustomThemeFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
{
// delete existing file
if (!string.IsNullOrEmpty(theme.CustomThemeFileId))
{
await _fileService.RemoveFile(theme.CustomThemeFileId, userId);
}
// add new file
try
{
var storedFile = await _fileService.AddFile(vm.CustomThemeFile, userId);
vm.CustomThemeFileId = theme.CustomThemeFileId = storedFile.Id;
theme.CustomThemeCssUrl = new UnresolvedUri.FileIdUri(storedFile.Id);
vm.CustomThemeCssUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), theme.CustomThemeCssUrl);
settingsChanged = true;
}
catch (Exception e)
@ -1095,10 +1092,12 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.CustomThemeFile), "The uploaded theme file needs to be a CSS file");
}
}
else if (RemoveCustomThemeFile && !string.IsNullOrEmpty(theme.CustomThemeFileId))
else if (RemoveCustomThemeFile && theme.CustomThemeCssUrl is not null)
{
await _fileService.RemoveFile(theme.CustomThemeFileId, userId);
vm.CustomThemeFileId = theme.CustomThemeFileId = null;
vm.CustomThemeCssUrl = null;
theme.CustomThemeCssUrl = null;
theme.CustomTheme = false;
theme.CustomThemeExtension = ThemeExtension.Custom;
settingsChanged = true;
}
@ -1122,16 +1121,12 @@ namespace BTCPayServer.Controllers
else
{
vm.LogoFile = formFile;
// delete existing file
if (!string.IsNullOrEmpty(theme.LogoFileId))
{
await _fileService.RemoveFile(theme.LogoFileId, userId);
}
// add new file
try
{
var storedFile = await _fileService.AddFile(vm.LogoFile, userId);
vm.LogoFileId = theme.LogoFileId = storedFile.Id;
theme.LogoUrl = new UnresolvedUri.FileIdUri(storedFile.Id);
vm.LogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), theme.LogoUrl);
settingsChanged = true;
}
catch (Exception e)
@ -1141,29 +1136,19 @@ namespace BTCPayServer.Controllers
}
}
}
else if (RemoveLogoFile && !string.IsNullOrEmpty(theme.LogoFileId))
else if (RemoveLogoFile && theme.LogoUrl is not null)
{
await _fileService.RemoveFile(theme.LogoFileId, userId);
vm.LogoFileId = theme.LogoFileId = null;
vm.LogoUrl = null;
theme.LogoUrl = null;
settingsChanged = true;
}
if (vm.CustomTheme && !string.IsNullOrEmpty(vm.CustomThemeCssUri) && !Uri.IsWellFormedUriString(vm.CustomThemeCssUri, UriKind.RelativeOrAbsolute))
{
ModelState.AddModelError(nameof(theme.CustomThemeCssUri), "Please provide a non-empty theme URI");
}
else if (theme.CustomThemeCssUri != vm.CustomThemeCssUri)
{
theme.CustomThemeCssUri = vm.CustomThemeCssUri;
settingsChanged = true;
}
if (theme.CustomThemeExtension != vm.CustomThemeExtension)
if (vm.CustomTheme && theme.CustomThemeExtension != vm.CustomThemeExtension)
{
// Require a custom theme to be defined in that case
if (string.IsNullOrEmpty(vm.CustomThemeCssUri) && string.IsNullOrEmpty(theme.CustomThemeFileId))
if (string.IsNullOrEmpty(vm.CustomThemeCssUrl) && theme.CustomThemeCssUrl is null)
{
ModelState.AddModelError(nameof(vm.CustomThemeFile), "Please provide a custom theme");
ModelState.AddModelError(nameof(vm.CustomThemeCssUrl), "Please provide a custom theme");
}
else
{
@ -1172,7 +1157,7 @@ namespace BTCPayServer.Controllers
}
}
if (theme.CustomTheme != vm.CustomTheme)
if (theme.CustomTheme != vm.CustomTheme && !RemoveCustomThemeFile)
{
theme.CustomTheme = vm.CustomTheme;
settingsChanged = true;
@ -1182,6 +1167,7 @@ namespace BTCPayServer.Controllers
{
await _SettingsRepository.UpdateSetting(theme);
TempData[WellKnownTempData.SuccessMessage] = "Settings updated successfully";
return RedirectToAction(nameof(Branding));
}
return View(vm);

View File

@ -95,8 +95,6 @@ namespace BTCPayServer.Controllers
{
Name = "",
Currency = CurrentStore.GetStoreBlob().DefaultCurrency,
CustomCSSLink = "",
EmbeddedCSS = "",
PayoutMethodsItem =
paymentMethods.Select(id => new SelectListItem(id.ToString(), id.ToString(), true))
});
@ -145,7 +143,7 @@ namespace BTCPayServer.Controllers
return View(model);
model.AutoApproveClaims = model.AutoApproveClaims && (await
_authorizationService.AuthorizeAsync(User, storeId, Policies.CanCreatePullPayments)).Succeeded;
await _pullPaymentService.CreatePullPayment(new HostedServices.CreatePullPayment()
await _pullPaymentService.CreatePullPayment(new CreatePullPayment
{
Name = model.Name,
Description = model.Description,
@ -153,17 +151,15 @@ namespace BTCPayServer.Controllers
Currency = model.Currency,
StoreId = storeId,
PayoutMethodIds = selectedPaymentMethodIds,
EmbeddedCSS = model.EmbeddedCSS,
CustomCSSLink = model.CustomCSSLink,
BOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration),
AutoApproveClaims = model.AutoApproveClaims
});
this.TempData.SetStatusMessageModel(new StatusMessageModel()
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Pull payment request created",
Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(PullPayments), new { storeId = storeId });
return RedirectToAction(nameof(PullPayments), new { storeId });
}
[Authorize(Policy = Policies.CanViewPullPayments, AuthenticationSchemes = AuthenticationSchemes.Cookie)]

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
@ -19,20 +20,19 @@ namespace BTCPayServer.Controllers;
public partial class UIStoresController
{
[HttpGet("{storeId}/settings")]
public IActionResult GeneralSettings()
public async Task<IActionResult> GeneralSettings()
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
var storeBlob = store.GetStoreBlob();
var vm = new GeneralSettingsViewModel
{
Id = store.Id,
StoreName = store.StoreName,
StoreWebsite = store.StoreWebsite,
LogoFileId = storeBlob.LogoFileId,
CssFileId = storeBlob.CssFileId,
LogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.LogoUrl),
CssUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.CssUrl),
BrandColor = storeBlob.BrandColor,
NetworkFeeMode = storeBlob.NetworkFeeMode,
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
@ -104,16 +104,11 @@ public partial class UIStoresController
else
{
model.LogoFile = formFile;
// delete existing file
if (!string.IsNullOrEmpty(blob.LogoFileId))
{
await _fileService.RemoveFile(blob.LogoFileId, userId);
}
// add new image
try
{
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
blob.LogoFileId = storedFile.Id;
blob.LogoUrl = new UnresolvedUri.FileIdUri(storedFile.Id);
}
catch (Exception e)
{
@ -122,10 +117,9 @@ public partial class UIStoresController
}
}
}
else if (RemoveLogoFile && !string.IsNullOrEmpty(blob.LogoFileId))
else if (RemoveLogoFile && blob.LogoUrl is not null)
{
await _fileService.RemoveFile(blob.LogoFileId, userId);
blob.LogoFileId = null;
blob.LogoUrl = null;
needUpdate = true;
}
@ -145,16 +139,11 @@ public partial class UIStoresController
}
else
{
// delete existing file
if (!string.IsNullOrEmpty(blob.CssFileId))
{
await _fileService.RemoveFile(blob.CssFileId, userId);
}
// add new file
try
{
var storedFile = await _fileService.AddFile(model.CssFile, userId);
blob.CssFileId = storedFile.Id;
blob.CssUrl = new UnresolvedUri.FileIdUri(storedFile.Id);
}
catch (Exception e)
{
@ -162,10 +151,9 @@ public partial class UIStoresController
}
}
}
else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId))
else if (RemoveCssFile && blob.CssUrl is not null)
{
await _fileService.RemoveFile(blob.CssFileId, userId);
blob.CssFileId = null;
blob.CssUrl = null;
needUpdate = true;
}
@ -221,7 +209,7 @@ public partial class UIStoresController
}
[HttpGet("{storeId}/checkout")]
public IActionResult CheckoutAppearance()
public async Task<IActionResult> CheckoutAppearance()
{
var storeBlob = CurrentStore.GetStoreBlob();
var vm = new CheckoutAppearanceViewModel();
@ -253,7 +241,9 @@ public partial class UIStoresController
vm.LightningAmountInSatoshi = storeBlob.LightningAmountInSatoshi;
vm.LazyPaymentMethods = storeBlob.LazyPaymentMethods;
vm.RedirectAutomatically = storeBlob.RedirectAutomatically;
vm.SoundFileId = storeBlob.SoundFileId;
vm.PaymentSoundUrl = storeBlob.PaymentSoundUrl is null
? string.Concat(Request.GetAbsoluteRootUri().ToString(), "checkout/payment.mp3")
: await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.PaymentSoundUrl);
vm.HtmlTitle = storeBlob.HtmlTitle;
vm.SupportUrl = storeBlob.StoreSupportUrl;
vm.DisplayExpirationTimer = (int)storeBlob.DisplayExpirationTimer.TotalMinutes;
@ -316,17 +306,11 @@ public partial class UIStoresController
else
{
model.SoundFile = formFile;
// delete existing file
if (!string.IsNullOrEmpty(blob.SoundFileId))
{
await _fileService.RemoveFile(blob.SoundFileId, userId);
}
// add new file
try
{
var storedFile = await _fileService.AddFile(model.SoundFile, userId);
blob.SoundFileId = storedFile.Id;
blob.PaymentSoundUrl = new UnresolvedUri.FileIdUri(storedFile.Id);
needUpdate = true;
}
catch (Exception e)
@ -336,10 +320,9 @@ public partial class UIStoresController
}
}
}
else if (RemoveSoundFile && !string.IsNullOrEmpty(blob.SoundFileId))
else if (RemoveSoundFile && blob.PaymentSoundUrl is not null)
{
await _fileService.RemoveFile(blob.SoundFileId, userId);
blob.SoundFileId = null;
blob.PaymentSoundUrl = null;
needUpdate = true;
}

View File

@ -56,6 +56,7 @@ public partial class UIStoresController : Controller
IHtmlHelper html,
EmailSenderFactory emailSenderFactory,
WalletFileParsers onChainWalletParsers,
UriResolver uriResolver,
SettingsRepository settingsRepository,
EventAggregator eventAggregator)
{
@ -78,6 +79,7 @@ public partial class UIStoresController : Controller
_externalServiceOptions = externalServiceOptions;
_emailSenderFactory = emailSenderFactory;
_onChainWalletParsers = onChainWalletParsers;
_uriResolver = uriResolver;
_settingsRepository = settingsRepository;
_eventAggregator = eventAggregator;
_html = html;
@ -106,6 +108,7 @@ public partial class UIStoresController : Controller
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly WalletFileParsers _onChainWalletParsers;
private readonly UriResolver _uriResolver;
private readonly EventAggregator _eventAggregator;
private readonly IHtmlHelper _html;
private readonly WebhookSender _webhookNotificationManager;

View File

@ -35,9 +35,7 @@ namespace BTCPayServer.Data
{
public string Title { get; set; }
public string Description { get; set; }
public string EmbeddedCSS { get; set; }
public string Email { get; set; }
public string CustomCSSLink { get; set; }
}
}
}

View File

@ -11,8 +11,6 @@ using BTCPayServer.JsonConverters;
using BTCPayServer.Payments;
using BTCPayServer.Rating;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -217,8 +215,10 @@ namespace BTCPayServer.Data
public List<UIStoresController.StoreEmailRule> EmailRules { get; set; }
public string BrandColor { get; set; }
public string LogoFileId { get; set; }
public string CssFileId { get; set; }
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
public UnresolvedUri LogoUrl { get; set; }
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
public UnresolvedUri CssUrl { get; set; }
[DefaultValue(true)]
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
@ -236,7 +236,8 @@ namespace BTCPayServer.Data
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
public bool PlaySoundOnPayment { get; set; }
public string SoundFileId { get; set; }
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
public UnresolvedUri PaymentSoundUrl { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{

View File

@ -14,6 +14,7 @@ using BTCPayServer.Data;
using BTCPayServer.Filters;
using BTCPayServer.Forms.Models;
using BTCPayServer.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -26,15 +27,18 @@ namespace BTCPayServer.Forms;
public class UIFormsController : Controller
{
private readonly FormDataService _formDataService;
private readonly UriResolver _uriResolver;
private readonly IAuthorizationService _authorizationService;
private readonly StoreRepository _storeRepository;
private FormComponentProviders FormProviders { get; }
public UIFormsController(FormComponentProviders formProviders, FormDataService formDataService,
UriResolver uriResolver,
StoreRepository storeRepository, IAuthorizationService authorizationService)
{
FormProviders = formProviders;
_formDataService = formDataService;
_uriResolver = uriResolver;
_authorizationService = authorizationService;
_storeRepository = storeRepository;
}
@ -169,7 +173,7 @@ public class UIFormsController : Controller
FormName = formData.Name,
Form = form,
StoreName = store?.StoreName,
StoreBranding = new StoreBrandingViewModel(storeBlob)
StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob)
});
}

View File

@ -38,8 +38,6 @@ namespace BTCPayServer.HostedServices
public string Description { get; set; }
public decimal Amount { get; set; }
public string Currency { get; set; }
public string CustomCSSLink { get; set; }
public string EmbeddedCSS { get; set; }
public PayoutMethodId[] PayoutMethodIds { get; set; }
public bool AutoApproveClaims { get; set; }
public TimeSpan? BOLT11Expiration { get; set; }
@ -131,13 +129,11 @@ namespace BTCPayServer.HostedServices
Limit = create.Amount,
SupportedPaymentMethods = create.PayoutMethodIds,
AutoApproveClaims = create.AutoApproveClaims,
View = new PullPaymentBlob.PullPaymentView()
View = new PullPaymentBlob.PullPaymentView
{
Title = create.Name ?? string.Empty,
Description = create.Description ?? string.Empty,
CustomCSSLink = create.CustomCSSLink,
Email = null,
EmbeddedCSS = create.EmbeddedCSS,
Email = null
},
BOLT11Expiration = create.BOLT11Expiration ?? TimeSpan.FromDays(30.0)
});

View File

@ -168,6 +168,7 @@ namespace BTCPayServer.Hosting
services.TryAddSingleton<EventAggregator>();
services.TryAddSingleton<PaymentRequestService>();
services.TryAddSingleton<UserService>();
services.TryAddSingleton<UriResolver>();
services.TryAddSingleton<WalletHistogramService>();
services.AddSingleton<ApplicationDbContextFactory>();
services.AddOptions<BTCPayServerOptions>().Configure(

View File

@ -4,9 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Common;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Client.Models;
using BTCPayServer.Configuration;
using BTCPayServer.Data;
using BTCPayServer.Fido2;
@ -26,17 +24,14 @@ using BTCPayServer.Services.Stores;
using BTCPayServer.Storage.Models;
using BTCPayServer.Storage.Services.Providers.FileSystemStorage.Configuration;
using Fido2NetLib.Objects;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PeterO.Cbor;
using YamlDotNet.RepresentationModel;
using LightningAddressData = BTCPayServer.Data.LightningAddressData;
using Serializer = NBXplorer.Serializer;
namespace BTCPayServer.Hosting
{
@ -53,6 +48,7 @@ namespace BTCPayServer.Hosting
private readonly LightningAddressService _lightningAddressService;
private readonly ILogger<MigrationStartupTask> _logger;
private readonly LightningClientFactoryService _lightningClientFactoryService;
private readonly IFileService _fileService;
public IOptions<LightningNetworkOptions> LightningOptions { get; }
@ -67,6 +63,7 @@ namespace BTCPayServer.Hosting
BTCPayNetworkJsonSerializerSettings btcPayNetworkJsonSerializerSettings,
LightningAddressService lightningAddressService,
ILogger<MigrationStartupTask> logger,
IFileService fileService,
LightningClientFactoryService lightningClientFactoryService)
{
_handlers = handlers;
@ -78,6 +75,7 @@ namespace BTCPayServer.Hosting
_btcPayNetworkJsonSerializerSettings = btcPayNetworkJsonSerializerSettings;
_lightningAddressService = lightningAddressService;
_logger = logger;
_fileService = fileService;
_lightningClientFactoryService = lightningClientFactoryService;
LightningOptions = lightningOptions;
}

View File

@ -0,0 +1,29 @@
using System;
using System.Reflection;
using BTCPayServer.Payouts;
using NBitcoin.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.JsonConverters
{
public class UnresolvedUriJsonConverter : JsonConverter<UnresolvedUri>
{
public override UnresolvedUri ReadJson(JsonReader reader, Type objectType, UnresolvedUri existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
if (reader.TokenType != JsonToken.String)
throw new JsonObjectException("A UnresolvedUri should be a string", reader);
var str = (string)reader.Value;
if (str.Length == 0)
return null;
return UnresolvedUri.Create(str);
}
public override void WriteJson(JsonWriter writer, UnresolvedUri value, JsonSerializer serializer)
{
if (value != null)
writer.WriteValue(value.ToString());
}
}
}

View File

@ -48,8 +48,6 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
Description = blob.Description;
ExpiryDate = blob.ExpiryDate?.UtcDateTime;
Email = blob.Email;
CustomCSSLink = blob.CustomCSSLink;
EmbeddedCSS = blob.EmbeddedCSS;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
FormResponse = blob.FormResponse is null
? null
@ -85,13 +83,6 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
[MailboxAddress]
public string Email { get; set; }
[MaxLength(500)]
[Display(Name = "Custom CSS URL")]
public string CustomCSSLink { get; set; }
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
[Display(Name = "Allow payee to create invoices with custom amounts")]
public bool AllowCustomPaymentAmounts { get; set; }
@ -115,8 +106,6 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
Description = blob.Description;
ExpiryDate = blob.ExpiryDate?.UtcDateTime;
Email = blob.Email;
EmbeddedCSS = blob.EmbeddedCSS;
CustomCSSLink = blob.CustomCSSLink;
AllowCustomPaymentAmounts = blob.AllowCustomPaymentAmounts;
switch (data.Status)
{
@ -154,8 +143,6 @@ namespace BTCPayServer.Models.PaymentRequestViewModels
public string Description { get; set; }
public string StoreName { get; set; }
public string StoreWebsite { get; set; }
public string EmbeddedCSS { get; set; }
public string CustomCSSLink { get; set; }
#nullable enable
public class InvoiceList : List<PaymentRequestInvoice>

View File

@ -21,20 +21,15 @@ public class BrandingViewModel
[Display(Name = "Custom Theme Extension Type")]
public ThemeExtension CustomThemeExtension { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[MaxLength(500)]
[Display(Name = "Custom Theme CSS URL")]
public string CustomThemeCssUri { get; set; }
[Display(Name = "Custom Theme File")]
[JsonIgnore]
public IFormFile CustomThemeFile { get; set; }
public string CustomThemeFileId { get; set; }
public string CustomThemeCssUrl { get; set; }
[Display(Name = "Logo")]
[JsonIgnore]
public IFormFile LogoFile { get; set; }
public string LogoFileId { get; set; }
public string LogoUrl { get; set; }
}

View File

@ -1,24 +1,32 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Models;
public class StoreBrandingViewModel
{
public string BrandColor { get; set; }
public string LogoFileId { get; set; }
public string CssFileId { get; set; }
public string CustomCSSLink { get; set; }
public string EmbeddedCSS { get; set; }
public string LogoUrl { get; set; }
public string CssUrl { get; set; }
public StoreBrandingViewModel()
{
}
public StoreBrandingViewModel(StoreBlob storeBlob)
public static async Task<StoreBrandingViewModel> CreateAsync(HttpRequest request, UriResolver uriResolver, StoreBlob storeBlob)
{
if (storeBlob == null)
return new StoreBrandingViewModel();
var result = new StoreBrandingViewModel(storeBlob);
result.LogoUrl = await uriResolver.Resolve(request.GetAbsoluteRootUri(), storeBlob.LogoUrl);
result.CssUrl = await uriResolver.Resolve(request.GetAbsoluteRootUri(), storeBlob.CssUrl);
return result;
}
private StoreBrandingViewModel(StoreBlob storeBlob)
{
if (storeBlob == null) return;
BrandColor = storeBlob.BrandColor;
LogoFileId = storeBlob.LogoFileId;
CssFileId = storeBlob.CssFileId;
}
}

View File

@ -59,7 +59,7 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Custom sound file for successful payment")]
public IFormFile SoundFile { get; set; }
public string SoundFileId { get; set; }
public string PaymentSoundUrl { get; set; }
[Display(Name = "Custom HTML title to display on Checkout page")]
public string HtmlTitle { get; set; }

View File

@ -27,11 +27,11 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Logo")]
public IFormFile LogoFile { get; set; }
public string LogoFileId { get; set; }
public string LogoUrl { get; set; }
[Display(Name = "Custom CSS")]
public IFormFile CssFile { get; set; }
public string CssFileId { get; set; }
public string CssUrl { get; set; }
public bool Archived { get; set; }

View File

@ -55,11 +55,6 @@ namespace BTCPayServer.Models.WalletViewModels
[Required]
[ReadOnly(true)]
public string Currency { get; set; }
[MaxLength(500)]
[Display(Name = "Custom CSS URL")]
public string CustomCSSLink { get; set; }
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
[Display(Name = "Payout Methods")]
public IEnumerable<string> PayoutMethods { get; set; }
@ -91,8 +86,6 @@ namespace BTCPayServer.Models.WalletViewModels
var blob = data.GetBlob();
Name = blob.Name;
Description = blob.Description;
CustomCSSLink = blob.View.CustomCSSLink;
EmbeddedCSS = blob.View.EmbeddedCSS;
}
[MaxLength(30)]
@ -100,11 +93,5 @@ namespace BTCPayServer.Models.WalletViewModels
[Display(Name = "Memo")]
public string Description { get; set; }
[Display(Name = "Custom CSS URL")]
public string CustomCSSLink { get; set; }
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
}
}

View File

@ -16,6 +16,7 @@ using BTCPayServer.Forms.Models;
using BTCPayServer.Models;
using BTCPayServer.Plugins.Crowdfund.Models;
using BTCPayServer.Plugins.PointOfSale.Models;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
@ -41,6 +42,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
AppService appService,
CurrencyNameTable currencies,
EventAggregator eventAggregator,
UriResolver uriResolver,
StoreRepository storeRepository,
UIInvoiceController invoiceController,
UserManager<ApplicationUser> userManager,
@ -53,11 +55,13 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
_app = app;
_storeRepository = storeRepository;
_eventAggregator = eventAggregator;
_uriResolver = uriResolver;
_invoiceController = invoiceController;
FormDataService = formDataService;
}
private readonly EventAggregator _eventAggregator;
private readonly UriResolver _uriResolver;
private readonly CurrencyNameTable _currencies;
private readonly StoreRepository _storeRepository;
private readonly AppService _appService;
@ -315,7 +319,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
var vm = new FormViewModel
{
StoreName = store.StoreName,
StoreBranding = new StoreBrandingViewModel(storeBlob),
StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob),
FormName = formData.Name,
Form = form,
AspController = controller,
@ -403,10 +407,8 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
TargetCurrency = settings.TargetCurrency,
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
EmbeddedCSS = settings.EmbeddedCSS,
EndDate = settings.EndDate,
TargetAmount = settings.TargetAmount,
CustomCSSLink = settings.CustomCSSLink,
NotificationUrl = settings.NotificationUrl,
Tagline = settings.Tagline,
PerksTemplate = settings.PerksTemplate,
@ -518,9 +520,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
Description = vm.Description,
EndDate = vm.EndDate?.ToUniversalTime(),
TargetAmount = vm.TargetAmount,
CustomCSSLink = vm.CustomCSSLink,
MainImageUrl = vm.MainImageUrl,
EmbeddedCSS = vm.EmbeddedCSS,
NotificationUrl = vm.NotificationUrl,
Tagline = vm.Tagline,
PerksTemplate = vm.PerksTemplate,
@ -580,6 +580,7 @@ namespace BTCPayServer.Plugins.Crowdfund.Controllers
return null;
}
var info = (ViewCrowdfundViewModel)await _app.GetInfo(app);
info.StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, app.StoreData.GetStoreBlob());
info.HubPath = AppHub.GetHubPath(Request);
info.SimpleDisplay = Request.Query.ContainsKey("simple");
return info;

View File

@ -18,6 +18,7 @@ using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Rates;
using Ganss.Xss;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
@ -180,11 +181,6 @@ namespace BTCPayServer.Plugins.Crowdfund
var store = appData.StoreData;
var storeBlob = store.GetStoreBlob();
var storeBranding = new StoreBrandingViewModel(storeBlob)
{
CustomCSSLink = settings.CustomCSSLink,
EmbeddedCSS = settings.EmbeddedCSS
};
var formUrl = settings.FormId != null
? _linkGenerator.GetPathByAction(nameof(UICrowdfundController.CrowdfundForm), "UICrowdfund",
new { appId = appData.Id }, _options.Value.RootPath)
@ -196,7 +192,6 @@ namespace BTCPayServer.Plugins.Crowdfund
Description = settings.Description,
MainImageUrl = settings.MainImageUrl,
StoreName = store.StoreName,
StoreBranding = storeBranding,
StoreId = appData.StoreDataId,
AppId = appData.Id,
StartDate = settings.StartDate?.ToUniversalTime(),

View File

@ -86,13 +86,6 @@ namespace BTCPayServer.Plugins.Crowdfund.Models
[Display(Name = "Contribution Perks Template")]
public string PerksTemplate { get; set; }
[MaxLength(500)]
[Display(Name = "Custom CSS URL")]
public string CustomCSSLink { get; set; }
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
[Display(Name = "Count all invoices created on the store as part of the goal")]
public bool UseAllStoreInvoices { get; set; }

View File

@ -47,6 +47,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
AppService appService,
CurrencyNameTable currencies,
StoreRepository storeRepository,
UriResolver uriResolver,
InvoiceRepository invoiceRepository,
UIInvoiceController invoiceController,
FormDataService formDataService,
@ -55,6 +56,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
_currencies = currencies;
_appService = appService;
_storeRepository = storeRepository;
_uriResolver = uriResolver;
_invoiceRepository = invoiceRepository;
_invoiceController = invoiceController;
_displayFormatter = displayFormatter;
@ -64,6 +66,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
private readonly CurrencyNameTable _currencies;
private readonly InvoiceRepository _invoiceRepository;
private readonly StoreRepository _storeRepository;
private readonly UriResolver _uriResolver;
private readonly AppService _appService;
private readonly UIInvoiceController _invoiceController;
private readonly DisplayFormatter _displayFormatter;
@ -86,13 +89,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
viewType ??= settings.EnableShoppingCart ? PosViewType.Cart : settings.DefaultView;
var store = await _appService.GetStore(app);
var storeBlob = store.GetStoreBlob();
var storeBranding = new StoreBrandingViewModel(storeBlob)
{
EmbeddedCSS = settings.EmbeddedCSS,
CustomCSSLink = settings.CustomCSSLink
};
// Check if the currency is COP or ARS (exclude decimal places)
var storeBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob);
return View($"PointOfSale/Public/{viewType}", new ViewPointOfSaleViewModel
{
@ -466,7 +463,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
var vm = new FormViewModel
{
StoreName = store.StoreName,
StoreBranding = new StoreBrandingViewModel(storeBlob),
StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob),
FormName = formData.Name,
Form = form,
AspController = controller,
@ -529,7 +526,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
viewModel.FormName = formData.Name;
viewModel.Form = form;
viewModel.FormParameters = formParameters;
viewModel.StoreBranding = new StoreBrandingViewModel(storeBlob);
viewModel.StoreBranding = await StoreBrandingViewModel.CreateAsync(Request, _uriResolver, storeBlob);
return View("Views/UIForms/View", viewModel);
}
@ -598,8 +595,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomTipPercentages = settings.CustomTipPercentages != null ? string.Join(",", settings.CustomTipPercentages) : string.Join(",", PointOfSaleSettings.CUSTOM_TIP_PERCENTAGES_DEF),
CustomCSSLink = settings.CustomCSSLink,
EmbeddedCSS = settings.EmbeddedCSS,
Description = settings.Description,
NotificationUrl = settings.NotificationUrl,
RedirectUrl = settings.RedirectUrl,
@ -689,11 +684,9 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = ListSplit(vm.CustomTipPercentages),
CustomCSSLink = vm.CustomCSSLink,
NotificationUrl = vm.NotificationUrl,
RedirectUrl = vm.RedirectUrl,
Description = vm.Description,
EmbeddedCSS = vm.EmbeddedCSS,
RedirectAutomatically = string.IsNullOrEmpty(vm.RedirectAutomatically) ? null : bool.Parse(vm.RedirectAutomatically),
FormId = vm.FormId
};

View File

@ -67,10 +67,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
[Display(Name = "Tip percentage amounts (comma separated)")]
public string CustomTipPercentages { get; set; }
[MaxLength(500)]
[Display(Name = "Custom CSS URL")]
public string CustomCSSLink { get; set; }
public string Id { get; set; }
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
@ -99,8 +95,6 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
}
}, nameof(SelectListItem.Value), nameof(SelectListItem.Text), RedirectAutomatically);
[Display(Name = "Custom CSS Code")]
public string EmbeddedCSS { get; set; }
public string Description { get; set; }
[Display(Name = "Request customer data on checkout")]

View File

@ -17,6 +17,7 @@ using BTCPayServer.Services.Rates;
using BTCPayServer.Services.Stores;
using Dapper;
using Ganss.Xss;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using NBitcoin;
using NBitcoin.DataEncoders;

View File

@ -4,6 +4,7 @@ using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Http;
namespace BTCPayServer.Services.Apps
{

View File

@ -28,11 +28,9 @@ namespace BTCPayServer.Services.Apps
}
public bool EnforceTargetAmount { get; set; }
public string CustomCSSLink { get; set; }
public string MainImageUrl { get; set; }
public string NotificationUrl { get; set; }
public string Tagline { get; set; }
public string EmbeddedCSS { get; set; }
public string PerksTemplate { get; set; }
public bool DisqusEnabled { get; set; }
public bool SoundsEnabled { get; set; }

View File

@ -103,11 +103,6 @@ namespace BTCPayServer.Services.Apps
public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
public static readonly int[] CUSTOM_TIP_PERCENTAGES_DEF = new int[] { 15, 18, 20 };
public int[] CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES_DEF;
public string CustomCSSLink { get; set; }
public string EmbeddedCSS { get; set; }
public string Description { get; set; }
public string NotificationUrl { get; set; }
public string RedirectUrl { get; set; }

View File

@ -1,4 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations;
using BTCPayServer.JsonConverters;
using Newtonsoft.Json;
namespace BTCPayServer.Services;
@ -21,14 +23,11 @@ public class ThemeSettings
[Display(Name = "Custom Theme Extension Type")]
public ThemeExtension CustomThemeExtension { get; set; }
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
[MaxLength(500)]
[Display(Name = "Custom Theme CSS URL")]
public string CustomThemeCssUri { get; set; }
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
public UnresolvedUri CustomThemeCssUrl { get; set; }
public string CustomThemeFileId { get; set; }
public string LogoFileId { get; set; }
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
public UnresolvedUri LogoUrl { get; set; }
public bool FirstRun { get; set; } = true;
@ -37,9 +36,4 @@ public class ThemeSettings
// no logs
return string.Empty;
}
public string CssUri
{
get => CustomTheme ? CustomThemeCssUri : "/main/themes/default.css";
}
}

View File

@ -0,0 +1,37 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Security.AccessControl;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
namespace BTCPayServer.Services
{
public class UriResolver
{
private readonly IFileService _fileService;
public UriResolver(IFileService fileService)
{
_fileService = fileService;
}
/// <summary>
/// If <paramref name="url"/> is an absolute URL, returns it as is.
/// If <paramref name="url"/> starts with "fileid:ID", returns the URL of the file with the ID.
/// </summary>
/// <param name="baseUri"><see cref="BTCPayServer.Abstractions.Extensions.HttpRequestExtensions.GetAbsoluteRootUri"/></param>
/// <param name="uri"></param>
/// <returns></returns>
public async Task<string?> Resolve(Uri baseUri, UnresolvedUri? uri)
{
return uri switch
{
null => null,
UnresolvedUri.FileIdUri fileId => await _fileService.GetFileUrl(baseUri, fileId.FileId),
UnresolvedUri.Raw raw => raw.Uri,
_ => throw new NotSupportedException(uri.GetType().Name)
};
}
}
}

View File

@ -0,0 +1,25 @@
using System;
namespace BTCPayServer
{
public record UnresolvedUri
{
public static UnresolvedUri Create(string str)
{
ArgumentNullException.ThrowIfNull(str);
if (str.StartsWith("fileid:", StringComparison.OrdinalIgnoreCase))
{
return new FileIdUri(str.Substring("fileid:".Length));
}
return new Raw(str);
}
public record FileIdUri(string FileId) : UnresolvedUri
{
public override string ToString() => $"fileid:{FileId}";
}
public record Raw(string Uri) : UnresolvedUri
{
public override string ToString() => Uri;
}
}
}

View File

@ -311,36 +311,6 @@
</div>
</div>
</div>
@* We are deprecating the custom CSS options in favor of the store branding approach.
Display this section only if these values are set. *@
@if (!string.IsNullOrWhiteSpace(Model.CustomCSSLink) || !string.IsNullOrWhiteSpace(Model.EmbeddedCSS))
{
<div class="accordion-item">
<h2 class="accordion-header" id="additional-custom-css-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="CustomCSSLink" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
<vc:icon symbol="info" />
</a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group mb-4">
<label asp-for="EmbeddedCSS" class="form-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>

View File

@ -1,14 +1,4 @@
@model StoreBrandingViewModel
@using BTCPayServer.Abstractions.Contracts
@inject IFileService FileService
@{
var logoUrl = !string.IsNullOrEmpty(Model.LogoFileId)
? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.LogoFileId)
: null;
var cssUrl = !string.IsNullOrEmpty(Model.CssFileId)
? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CssFileId)
: null;
}
@if (!string.IsNullOrEmpty(Model.BrandColor))
{
var brand = Model.BrandColor;
@ -39,23 +29,12 @@
</style>
<meta name="theme-color" content="@brand">
}
@if (!string.IsNullOrEmpty(cssUrl))
@if (!string.IsNullOrEmpty(Model.CssUrl))
{
<link href="@cssUrl" asp-append-version="true" rel="stylesheet" />
<link href="@Model.CssUrl!" asp-append-version="true" rel="stylesheet" />
}
@* Deprecated, but added for backwards-compatibility *@
@if (!string.IsNullOrEmpty(Model.CustomCSSLink))
@if (!string.IsNullOrEmpty(Model.LogoUrl))
{
<link href="@Model.CustomCSSLink" asp-append-version="true" rel="stylesheet" />
}
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
{
<style>
@Safe.Raw(Model.EmbeddedCSS)
</style>
}
@if (!string.IsNullOrEmpty(logoUrl))
{
<link rel="icon" href="@logoUrl">
<link rel="apple-touch-icon" href="@logoUrl">
<link rel="icon" href="@Model.LogoUrl">
<link rel="apple-touch-icon" href="@Model.LogoUrl">
}

View File

@ -1,14 +1,9 @@
@using BTCPayServer.Services
@using BTCPayServer.Abstractions.Contracts
@inject ThemeSettings Theme
@inject IFileService FileService
@inject UriResolver UriResolver
<script>if (window.localStorage.getItem('btcpay-hide-sensitive-info') === 'true') { document.documentElement.setAttribute('data-hide-sensitive-info', 'true')}</script>
@if (Theme.CustomTheme && !string.IsNullOrEmpty(Theme.CssUri))
{ // legacy customization with CSS URI - keep it for backwards-compatibility
<link href="@Context.Request.GetRelativePathOrAbsolute(Theme.CssUri)" rel="stylesheet" asp-append-version="true" />
}
else if (Theme.CustomTheme && !string.IsNullOrEmpty(Theme.CustomThemeFileId))
@if (Theme.CustomTheme && Theme.CustomThemeCssUrl is not null)
{ // new customization uses theme file id provided by upload
@if (Theme.CustomThemeExtension != ThemeExtension.Custom)
{ // needs to be added for light and dark, because dark extends light
@ -17,8 +12,9 @@ else if (Theme.CustomTheme && !string.IsNullOrEmpty(Theme.CustomThemeFileId))
@if (Theme.CustomThemeExtension == ThemeExtension.Dark)
{
<link href="~/main/themes/default-dark.css" rel="stylesheet" asp-append-version="true" />
}
<link href="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Theme.CustomThemeFileId))" rel="stylesheet" asp-append-version="true" />
}
var themeUrl = await UriResolver.Resolve(this.Context.Request.GetAbsoluteRootUri(), Theme.CustomThemeCssUrl);
<link href="@themeUrl" rel="stylesheet" asp-append-version="true" />
}
else
{

View File

@ -289,36 +289,6 @@
</div>
</div>
</div>
@* We are deprecating the custom CSS options in favor of the store branding approach.
Display this section only if these values are set. *@
@if (!string.IsNullOrWhiteSpace(Model.CustomCSSLink) || !string.IsNullOrWhiteSpace(Model.EmbeddedCSS))
{
<div class="accordion-item">
<h2 class="accordion-header" id="additional-custom-css-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="CustomCSSLink" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
<vc:icon symbol="info" />
</a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EmbeddedCSS" class="form-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>

View File

@ -1,15 +1,9 @@
@inject IFileService FileService
@using BTCPayServer.Abstractions.Contracts
@model (string Title, StoreBrandingViewModel StoreBranding)
@{
var logoUrl = !string.IsNullOrEmpty(Model.StoreBranding.LogoFileId)
? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.StoreBranding.LogoFileId)
: null;
}
<header class="store-header" v-pre>
@if (!string.IsNullOrEmpty(logoUrl))
@if (!string.IsNullOrEmpty(Model.StoreBranding.LogoUrl))
{
<img src="@logoUrl" alt="@Model.Title" class="store-logo"/>
<img src="@Model.StoreBranding.LogoUrl" alt="@Model.Title" class="store-logo"/>
}
<h1 class="store-name">@Model.Title</h1>
</header>

View File

@ -15,7 +15,7 @@
<body class="min-vh-100">
<div id="FormView" class="public-page-wrap">
<partial name="_StatusMessage" model="@(new ViewDataDictionary(ViewData) { { "Margin", "mb-4" } })" />
@if (!string.IsNullOrEmpty(Model.StoreName) || !string.IsNullOrEmpty(Model.StoreBranding.LogoFileId))
@if (!string.IsNullOrEmpty(Model.StoreName) || !string.IsNullOrEmpty(Model.StoreBranding.LogoUrl))
{
<partial name="_StoreHeader" model="(Model.StoreName, Model.StoreBranding)" />
}

View File

@ -123,46 +123,6 @@
</div>
</div>
</div>
@* We are deprecating the custom CSS options in favor of the store branding approach.
Display this section only if these values are set. *@
@if (!string.IsNullOrWhiteSpace(Model.CustomCSSLink) || !string.IsNullOrWhiteSpace(Model.EmbeddedCSS))
{
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group">
<div class="accordion" id="additional">
<div class="accordion-item">
<h2 class="accordion-header" id="additional-custom-css-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="CustomCSSLink" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
<vc:icon symbol="info" />
</a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EmbeddedCSS" class="form-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
</form>
@if (!string.IsNullOrEmpty(Model.Id))

View File

@ -1,5 +1,4 @@
@using BTCPayServer.Views.Stores
@using BTCPayServer.Abstractions.Extensions
@model BTCPayServer.Models.WalletViewModels.UpdatePullPaymentModel
@{
@ -53,44 +52,4 @@
</div>
</div>
</div>
@* We are deprecating the custom CSS options in favor of the store branding approach.
Display this section only if these values are set. *@
@if (!string.IsNullOrWhiteSpace(Model.CustomCSSLink) || !string.IsNullOrWhiteSpace(Model.EmbeddedCSS))
{
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<h4 class="mt-5 mb-2">Additional Options</h4>
<div class="form-group">
<div class="accordion" id="additional">
<div class="accordion-item">
<h2 class="accordion-header" id="additional-custom-css-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="CustomCSSLink" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener">
<vc:icon symbol="info" />
</a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EmbeddedCSS" class="form-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
</form>

View File

@ -36,7 +36,7 @@
<div class="form-group">
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="LogoFile" class="form-label"></label>
@if (!string.IsNullOrEmpty(Model.LogoFileId))
@if (!string.IsNullOrEmpty(Model.LogoUrl))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveLogoFile" value="true">
<span class="fa fa-times"></span> Remove
@ -47,9 +47,9 @@
{
<div class="d-flex align-items-center gap-3">
<input asp-for="LogoFile" type="file" class="form-control flex-grow">
@if (!string.IsNullOrEmpty(Model.LogoFileId))
@if (!string.IsNullOrEmpty(Model.LogoUrl))
{
<img src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.LogoFileId))" alt="Logo" style="height:2.1rem;max-width:10.5rem;"/>
<img src="@Model.LogoUrl" alt="Logo" style="height:2.1rem;max-width:10.5rem;"/>
}
</div>
<span asp-validation-for="LogoFile" class="text-danger"></span>
@ -75,48 +75,37 @@
</div>
</div>
<div class="collapse @(Model.CustomTheme ? "show" : "")" id="CustomThemeSettings">
@if (!string.IsNullOrEmpty(Model.CustomThemeCssUri))
{
<div class="form-group mb-0 pt-2">
<label asp-for="CustomThemeCssUri" class="form-label" data-required></label>
<input asp-for="CustomThemeCssUri" class="form-control"/>
<span asp-validation-for="CustomThemeCssUri" class="text-danger"></span>
<div class="form-group pt-2">
<label asp-for="CustomThemeExtension" class="form-label" data-required></label>
<select asp-for="CustomThemeExtension" asp-items="@themeExtension" class="form-select w-auto"></select>
</div>
<div class="form-group mb-0">
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="CustomThemeFile" class="form-label" data-required></label>
@if (!string.IsNullOrEmpty(Model.CustomThemeCssUrl))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveCustomThemeFile" value="true">
<span class="fa fa-times"></span> Remove
</button>
}
</div>
}
else
{
<div class="form-group pt-2">
<label asp-for="CustomThemeExtension" class="form-label" data-required></label>
<select asp-for="CustomThemeExtension" asp-items="@themeExtension" class="form-select w-auto"></select>
</div>
<div class="form-group mb-0">
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="CustomThemeFile" class="form-label" data-required></label>
@if (!string.IsNullOrEmpty(Model.CustomThemeFileId))
@if (canUpload)
{
<div class="d-flex align-items-center gap-3">
<input asp-for="CustomThemeFile" type="file" class="form-control flex-grow">
@if (!string.IsNullOrEmpty(Model.CustomThemeCssUrl))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveCustomThemeFile" value="true">
<span class="fa fa-times"></span> Remove
</button>
<a href="@Model.CustomThemeCssUrl" target="_blank" rel="noreferrer noopener" class="text-nowrap">Custom CSS</a>
}
</div>
@if (canUpload)
{
<div class="d-flex align-items-center gap-3">
<input asp-for="CustomThemeFile" type="file" class="form-control flex-grow">
@if (!string.IsNullOrEmpty(Model.CustomThemeFileId))
{
<a href="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CustomThemeFileId))" target="_blank" rel="noreferrer noopener" class="text-nowrap">Custom CSS</a>
}
</div>
<span asp-validation-for="CustomThemeFile" class="text-danger"></span>
}
else
{
<input asp-for="CustomThemeFile" type="file" class="form-control" disabled>
<p class="form-text text-muted">In order to upload a theme file, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</p>
}
</div>
}
<span asp-validation-for="CustomThemeFile" class="text-danger"></span>
}
else
{
<input asp-for="CustomThemeFile" type="file" class="form-control" disabled>
<p class="form-text text-muted">In order to upload a theme file, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</p>
}
</div>
</div>
<button type="submit" class="btn btn-primary mt-2" name="command" value="Save">Save</button>

View File

@ -95,36 +95,6 @@
</div>
</div>
</div>
@* We are deprecating the custom CSS options in favor of the store branding approach.
Display this section only if these values are set. *@
@if (!string.IsNullOrWhiteSpace(Model.CustomCSSLink) || !string.IsNullOrWhiteSpace(Model.EmbeddedCSS))
{
<div class="accordion-item">
<h2 class="accordion-header" id="additional-custom-css-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#additional-custom-css" aria-expanded="false" aria-controls="additional-custom-css">
Custom CSS
<vc:icon symbol="caret-down" />
</button>
</h2>
<div id="additional-custom-css" class="accordion-collapse collapse" aria-labelledby="additional-custom-css-header">
<div class="accordion-body">
<div class="form-group">
<label asp-for="CustomCSSLink" class="form-label"></label>
<a href="https://docs.btcpayserver.org/Development/Theme/" target="_blank" rel="noreferrer noopener" title="More information...">
<vc:icon symbol="info" />
</a>
<input asp-for="CustomCSSLink" class="form-control" />
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="EmbeddedCSS" class="form-label"></label>
<textarea asp-for="EmbeddedCSS" rows="10" cols="40" class="form-control"></textarea>
<span asp-validation-for="EmbeddedCSS" class="text-danger"></span>
</div>
</div>
</div>
</div>
}
</div>
</div>
</div>

View File

@ -143,7 +143,7 @@
<div class="form-group mb-0 py-3">
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="SoundFile" class="form-label"></label>
@if (!string.IsNullOrEmpty(Model.SoundFileId))
@if (!string.IsNullOrEmpty(Model.PaymentSoundUrl))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveSoundFile" value="true" permission="@Policies.CanModifyStoreSettings">
<span class="fa fa-times"></span> Remove
@ -154,12 +154,7 @@
{
<div class="d-flex align-items-center gap-3">
<input asp-for="SoundFile" type="file" class="form-control flex-grow">
@{
var soundUrl = string.IsNullOrEmpty(Model.SoundFileId)
? string.Concat(Context.Request.GetAbsoluteRootUri().ToString(), "checkout/payment.mp3")
: await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.SoundFileId);
}
<audio controls src="@soundUrl" style="height:2.1rem;max-width:10.5rem;"></audio>
<audio controls src="@Model.PaymentSoundUrl" style="height:2.1rem;max-width:10.5rem;"></audio>
</div>
<span asp-validation-for="SoundFile" class="text-danger"></span>
}

View File

@ -46,7 +46,7 @@
<div class="form-group">
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="LogoFile" class="form-label"></label>
@if (!string.IsNullOrEmpty(Model.LogoFileId))
@if (!string.IsNullOrEmpty(Model.LogoUrl))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveLogoFile" value="true">
<span class="fa fa-times"></span> Remove
@ -57,9 +57,9 @@
{
<div class="d-flex align-items-center gap-3">
<input asp-for="LogoFile" type="file" class="form-control flex-grow">
@if (!string.IsNullOrEmpty(Model.LogoFileId))
@if (!string.IsNullOrEmpty(Model.LogoUrl))
{
<img src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.LogoFileId))" alt="@Model.StoreName Logo" style="height:2.1rem;max-width:10.5rem;"/>
<img src="@Model.LogoUrl" alt="@Model.StoreName Logo" style="height:2.1rem;max-width:10.5rem;"/>
}
</div>
<span asp-validation-for="LogoFile" class="text-danger"></span>
@ -73,7 +73,7 @@
<div class="form-group">
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="CssFile" class="form-label"></label>
@if (!string.IsNullOrEmpty(Model.CssFileId))
@if (!string.IsNullOrEmpty(Model.CssUrl))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveCssFile" value="true">
<span class="fa fa-times"></span> Remove
@ -84,9 +84,9 @@
{
<div class="d-flex align-items-center gap-3">
<input asp-for="CssFile" type="file" class="form-control flex-grow">
@if (!string.IsNullOrEmpty(Model.CssFileId))
@if (!string.IsNullOrEmpty(Model.CssUrl))
{
<a href="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CssFileId))" target="_blank" rel="noreferrer noopener" class="text-nowrap">Custom CSS</a>
<a href="@Model.CssUrl" target="_blank" rel="noreferrer noopener" class="text-nowrap">Custom CSS</a>
}
</div>
<span asp-validation-for="LogoFile" class="text-danger"></span>

View File

@ -551,11 +551,6 @@
"description": "Prompt which appears next to the tip amount field if tipping is enabled",
"example": "Do you want to leave a tip?"
},
"customCSSLink": {
"type": "string",
"description": "Link to a custom CSS stylesheet to be used in the app",
"example": "https://bootswatch.com/4/slate/bootstrap.min.css"
},
"notificationUrl": {
"type": "string",
"description": "Callback notification url to POST to once when invoice is paid for and once when there are enough blockchain confirmations"
@ -564,10 +559,6 @@
"type": "string",
"description": "URL user is redirected to once invoice is paid"
},
"embeddedCSS": {
"type": "string",
"description": "Custom CSS embedded into the app"
},
"redirectAutomatically": {
"type": "boolean",
"description": "Whether user is redirected to specified redirect URL automatically after the invoice is paid",
@ -627,18 +618,10 @@
"description": "Target amount for the crowdfund",
"example": 420.69
},
"customCSSLink": {
"type": "string",
"description": "Link to a custom CSS stylesheet to be used in the app"
},
"mainImageUrl": {
"type": "string",
"description": "URL for image used as a cover image for the app"
},
"embeddedCSS": {
"type": "string",
"description": "Custom CSS embedded into the app"
},
"perks": {
"type": "object",
"description": "JSON of perks available in the app",
@ -875,16 +858,6 @@
"default": "Do you want to leave a tip?",
"nullable": true
},
"customCSSLink": {
"type": "string",
"description": "Link to a custom CSS stylesheet to be used in the app",
"nullable": true
},
"embeddedCSS": {
"type": "string",
"description": "Custom CSS to embed into the app",
"nullable": true
},
"notificationUrl": {
"type": "string",
"description": "Callback notification url to POST to once when invoice is paid for and once when there are enough blockchain confirmations",
@ -966,21 +939,11 @@
"example": 420,
"nullable": true
},
"customCSSLink": {
"type": "string",
"description": "Link to a custom CSS stylesheet to be used in the app",
"nullable": true
},
"mainImageUrl": {
"type": "string",
"description": "URL for image to be used as a cover image for the app",
"nullable": true
},
"embeddedCSS": {
"type": "string",
"description": "Custom CSS to embed into the app",
"nullable": true
},
"perksTemplate": {
"type": "string",
"description": "YAML template of perks available in the app",

View File

@ -454,19 +454,6 @@
"nullable": true,
"allOf": [ { "$ref": "#/components/schemas/UnixTimestamp" } ]
},
"embeddedCSS": {
"type": "string",
"description": "Custom CSS styling for the payment request",
"nullable": true,
"format": "css",
"maximum": 500
},
"customCSSLink": {
"type": "string",
"description": "Custom CSS link for styling the payment request",
"nullable": true,
"format": "uri"
},
"allowCustomPaymentAmounts": {
"type": "boolean",
"description": "Whether to allow users to create invoices that partially pay the payment request ",

View File

@ -411,6 +411,30 @@
"description": "The support URI of the store, can contain the placeholders `{OrderId}` and `{InvoiceId}`. Can be any valid URI, such as a website, email, and nostr.",
"format": "uri"
},
"logoUrl": {
"type": "string",
"nullable": true,
"description": "Absolute URL to a logo file or a reference to an uploaded file id with `fileid:ID`",
"format": "uri"
},
"cssUrl": {
"type": "string",
"nullable": true,
"description": "Absolute URL to CSS file to customize the public/customer-facing pages of the store. (Invoice, Payment Request, Pull Payment, etc.) or a reference to an uploaded file id with `fileid:ID`",
"format": "uri"
},
"paymentSoundUrl": {
"type": "string",
"nullable": true,
"description": "Absolute URL to a sound file or a reference to an uploaded file id with `fileid:ID`",
"format": "uri"
},
"brandColor": {
"type": "string",
"description": "The brand color of the store in HEX format",
"nullable": true,
"example": "#F7931A"
},
"defaultCurrency": {
"type": "string",
"description": "The default currency of the store",