Store Branding: Apply brand color to backend as well (#5992)

* Store Branding: Apply brand color to backend as well

Closes #5990.

* Add adjustments for different theme scenarios

* Add description text

* Make it optional to apply the brand color to the backend

* Toggle color fixes
This commit is contained in:
d11n 2024-09-13 14:39:21 +02:00 committed by GitHub
parent b7ba53eb60
commit 7348a6a62f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 164 additions and 35 deletions

View File

@ -17,6 +17,7 @@ namespace BTCPayServer.Client.Models
public string Website { get; set; }
public string BrandColor { get; set; }
public bool ApplyBrandColorToBackend { get; set; }
public string LogoUrl { get; set; }
public string CssUrl { get; set; }
public string PaymentSoundUrl { get; set; }

View File

@ -1622,6 +1622,7 @@ namespace BTCPayServer.Tests
CssUrl = "https://example.org/style.css",
LogoUrl = "https://example.org/logo.svg",
BrandColor = "#003366",
ApplyBrandColorToBackend = true,
PaymentMethodCriteria = new List<PaymentMethodCriteriaData>
{
new()
@ -1637,6 +1638,7 @@ namespace BTCPayServer.Tests
Assert.Equal("https://example.org/style.css", updatedStore.CssUrl);
Assert.Equal("https://example.org/logo.svg", updatedStore.LogoUrl);
Assert.Equal("#003366", updatedStore.BrandColor);
Assert.True(updatedStore.ApplyBrandColorToBackend);
var s = (await client.GetStore(newStore.Id));
Assert.Equal("B", s.Name);
var pmc = Assert.Single(s.PaymentMethodCriteria);

View File

@ -175,6 +175,7 @@ namespace BTCPayServer.Controllers.Greenfield
Website = data.StoreWebsite,
Archived = data.Archived,
BrandColor = storeBlob.BrandColor,
ApplyBrandColorToBackend = storeBlob.ApplyBrandColorToBackend,
CssUrl = storeBlob.CssUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.CssUrl),
LogoUrl = storeBlob.LogoUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.LogoUrl),
PaymentSoundUrl = storeBlob.PaymentSoundUrl == null ? null : await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.PaymentSoundUrl),
@ -255,6 +256,7 @@ namespace BTCPayServer.Controllers.Greenfield
blob.PaymentTolerance = restModel.PaymentTolerance;
blob.PayJoinEnabled = restModel.PayJoinEnabled;
blob.BrandColor = restModel.BrandColor;
blob.ApplyBrandColorToBackend = restModel.ApplyBrandColorToBackend;
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);

View File

@ -34,6 +34,7 @@ public partial class UIStoresController
LogoUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.LogoUrl),
CssUrl = await _uriResolver.Resolve(Request.GetAbsoluteRootUri(), storeBlob.CssUrl),
BrandColor = storeBlob.BrandColor,
ApplyBrandColorToBackend = storeBlob.ApplyBrandColorToBackend,
NetworkFeeMode = storeBlob.NetworkFeeMode,
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
PaymentTolerance = storeBlob.PaymentTolerance,
@ -75,10 +76,11 @@ public partial class UIStoresController
blob.RefundBOLT11Expiration = TimeSpan.FromDays(model.BOLT11Expiration);
if (!string.IsNullOrEmpty(model.BrandColor) && !ColorPalette.IsValid(model.BrandColor))
{
ModelState.AddModelError(nameof(model.BrandColor), "Invalid color");
ModelState.AddModelError(nameof(model.BrandColor), "The brand color needs to be a valid hex color code");
return View(model);
}
blob.BrandColor = model.BrandColor;
blob.ApplyBrandColorToBackend = model.ApplyBrandColorToBackend && !string.IsNullOrEmpty(model.BrandColor);
var userId = GetUserId();
if (userId is null)

View File

@ -191,6 +191,8 @@ namespace BTCPayServer.Data
public List<UIStoresController.StoreEmailRule> EmailRules { get; set; }
public string BrandColor { get; set; }
public bool ApplyBrandColorToBackend { get; set; }
[JsonConverter(typeof(UnresolvedUriJsonConverter))]
public UnresolvedUri LogoUrl { get; set; }
[JsonConverter(typeof(UnresolvedUriJsonConverter))]

View File

@ -1,4 +1,3 @@
using System;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
@ -10,6 +9,7 @@ namespace BTCPayServer.Models;
public class StoreBrandingViewModel
{
public string BrandColor { get; set; }
public bool ApplyBrandColorToBackend { get; set; }
public string LogoUrl { get; set; }
public string CssUrl { get; set; }
@ -20,13 +20,16 @@ public class StoreBrandingViewModel
{
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);
var result = new StoreBrandingViewModel(storeBlob)
{
LogoUrl = await uriResolver.Resolve(request.GetAbsoluteRootUri(), storeBlob.LogoUrl),
CssUrl = await uriResolver.Resolve(request.GetAbsoluteRootUri(), storeBlob.CssUrl)
};
return result;
}
private StoreBrandingViewModel(StoreBlob storeBlob)
{
BrandColor = storeBlob.BrandColor;
ApplyBrandColorToBackend = storeBlob.ApplyBrandColorToBackend;
}
}

View File

@ -25,6 +25,9 @@ namespace BTCPayServer.Models.StoreViewModels
[Display(Name = "Brand Color")]
public string BrandColor { get; set; }
[Display(Name = "Apply the brand color to the store's backend as well")]
public bool ApplyBrandColorToBackend { get; set; }
[Display(Name = "Logo")]
public IFormFile LogoFile { get; set; }
public string LogoUrl { get; set; }

View File

@ -1,4 +1,17 @@
@inject BTCPayServer.Services.PoliciesSettings PoliciesSettings
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@inject PoliciesSettings PoliciesSettings
@inject UriResolver UriResolver
@{
ViewData.TryGetValue("StoreBranding", out var storeBranding);
var store = Context.GetStoreData();
var storeBlob = store?.GetStoreBlob();
var isBackend = store != null && storeBranding == null;
if (isBackend && storeBlob.ApplyBrandColorToBackend)
{
storeBranding = await StoreBrandingViewModel.CreateAsync(Context.Request, UriResolver, storeBlob);
}
}
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@if (PoliciesSettings.DiscourageSearchEngines)
@ -14,7 +27,7 @@
<link href="~/main/site.css" asp-append-version="true" rel="stylesheet" />
<partial name="LayoutHeadTheme" />
@if (ViewData.TryGetValue("StoreBranding", out var storeBranding) && storeBranding != null)
@if (storeBranding != null)
{
<partial name="LayoutHeadStoreBranding" model="storeBranding" />
}

View File

@ -1,10 +1,17 @@
@using BTCPayServer.Services
@using Microsoft.AspNetCore.Mvc.TagHelpers
@model StoreBrandingViewModel
@inject ThemeSettings Theme
@if (!string.IsNullOrEmpty(Model.BrandColor))
{
var hasCustomeTheme = Theme.CustomTheme && Theme.CustomThemeCssUrl is not null;
var brand = Model.BrandColor;
var brandColor = ColorPalette.Default.FromHtml(brand);
var brandRgbValues = $"{brandColor.R}, {brandColor.G}, {brandColor.B}";
var accent = ColorPalette.Default.AdjustBrightness(brand, (float)-0.15);
var brightness = brandColor.GetBrightness();
var accent = ColorPalette.Default.AdjustBrightness(brand, (float)-.15);
var accentColor = ColorPalette.Default.FromHtml(accent);
var accentRgbValues = $"{accentColor.R}, {accentColor.G}, {accentColor.B}";
var complement = ColorPalette.Default.TextColor(brand);
var complementVar = $"var(--btcpay-{(complement == "black" ? "black" : "white")})";
<style>
@ -14,19 +21,93 @@
--btcpay-primary-shadow: @brand;
--btcpay-primary-bg-hover: @accent;
--btcpay-primary-bg-active: @accent;
--btcpay-body-link: @brand;
--btcpay-body-link-accent: @accent;
--btcpay-body-link-accent-rgb: @accentRgbValues;
--btcpay-primary-text: @complementVar;
--btcpay-primary-text-hover: @complementVar;
--btcpay-primary-text-active: @complementVar;
}
a {
color: var(--btcpay-body-link);
}
a:hover {
color: var(--btcpay-body-link-accent);
}
</style>
@if (brightness > .5 || (Theme.CustomThemeExtension == ThemeExtension.Dark && brightness < .5))
{
var brandAdjusted = ColorPalette.Default.AdjustBrightness(brand, (float)(.35-brightness));
var brandColorAdjusted = ColorPalette.Default.FromHtml(brandAdjusted);
var brandRgbValuesAdjusted = $"{brandColorAdjusted.R}, {brandColorAdjusted.G}, {brandColorAdjusted.B}";
var accentAdjusted = ColorPalette.Default.AdjustBrightness(brandAdjusted, (float)-.15);
var accentColorAdjusted = ColorPalette.Default.FromHtml(accentAdjusted);
var accentRgbValuesAdjusted = $"{accentColorAdjusted.R}, {accentColorAdjusted.G}, {accentColorAdjusted.B}";
var complementAdjusted = ColorPalette.Default.TextColor(brandAdjusted);
var complementVarAdjusted = $"var(--btcpay-{(complementAdjusted == "black" ? "black" : "white")})";
<style>
:root[data-theme='light'],
:root[data-btcpay-theme='light'] {
--btcpay-primary: @brandAdjusted;
--btcpay-primary-rgb: @brandRgbValuesAdjusted;
--btcpay-primary-shadow: @brandAdjusted;
--btcpay-primary-bg-hover: @accentAdjusted;
--btcpay-primary-bg-active: @accentAdjusted;
--btcpay-body-link-accent: @accentAdjusted;
--btcpay-body-link-accent-rgb: @accentRgbValuesAdjusted;
--btcpay-primary-text: @complementVarAdjusted;
--btcpay-primary-text-hover: @complementVarAdjusted;
--btcpay-primary-text-active: @complementVarAdjusted;
}
@@media (prefers-color-scheme: light) {
:root:not([data-btcpay-theme], [data-theme]) {
--btcpay-primary: @brandAdjusted;
--btcpay-primary-rgb: @brandRgbValuesAdjusted;
--btcpay-primary-shadow: @brandAdjusted;
--btcpay-primary-bg-hover: @accentAdjusted;
--btcpay-primary-bg-active: @accentAdjusted;
--btcpay-body-link-accent: @accentAdjusted;
--btcpay-body-link-accent-rgb: @accentRgbValuesAdjusted;
--btcpay-primary-text: @complementVarAdjusted;
--btcpay-primary-text-hover: @complementVarAdjusted;
--btcpay-primary-text-active: @complementVarAdjusted;
}
}
</style>
}
@if (brightness < .5 && (!hasCustomeTheme || Theme.CustomThemeExtension == ThemeExtension.Dark))
{
var brandAdjusted = ColorPalette.Default.AdjustBrightness(brand, (float)(.5-brightness));
var brandColorAdjusted = ColorPalette.Default.FromHtml(brandAdjusted);
var brandRgbValuesAdjusted = $"{brandColorAdjusted.R}, {brandColorAdjusted.G}, {brandColorAdjusted.B}";
var accentAdjusted = ColorPalette.Default.AdjustBrightness(brandAdjusted, (float).15);
var accentColorAdjusted = ColorPalette.Default.FromHtml(accentAdjusted);
var accentRgbValuesAdjusted = $"{accentColorAdjusted.R}, {accentColorAdjusted.G}, {accentColorAdjusted.B}";
var complementAdjusted = ColorPalette.Default.TextColor(brandAdjusted);
var complementVarAdjusted = $"var(--btcpay-{(complementAdjusted == "black" ? "black" : "white")})";
<style>
:root[data-theme='dark'],
:root[data-btcpay-theme='dark'] {
--btcpay-primary: @brandAdjusted;
--btcpay-primary-rgb: @brandRgbValuesAdjusted;
--btcpay-primary-shadow: @brandAdjusted;
--btcpay-primary-bg-hover: @accentAdjusted;
--btcpay-primary-bg-active: @accentAdjusted;
--btcpay-body-link-accent: @accentAdjusted;
--btcpay-body-link-accent-rgb: @accentRgbValuesAdjusted;
--btcpay-primary-text: @complementVarAdjusted;
--btcpay-primary-text-hover: @complementVarAdjusted;
--btcpay-primary-text-active: @complementVarAdjusted;
}
@@media (prefers-color-scheme: dark) {
:root:not([data-btcpay-theme], [data-theme]) {
--btcpay-primary: @brandAdjusted;
--btcpay-primary-rgb: @brandRgbValuesAdjusted;
--btcpay-primary-shadow: @brandAdjusted;
--btcpay-primary-bg-hover: @accentAdjusted;
--btcpay-primary-bg-active: @accentAdjusted;
--btcpay-body-link-accent: @accentAdjusted;
--btcpay-body-link-accent-rgb: @accentRgbValuesAdjusted;
--btcpay-primary-text: @complementVarAdjusted;
--btcpay-primary-text-hover: @complementVarAdjusted;
--btcpay-primary-text-active: @complementVarAdjusted;
}
}
</style>
}
<meta name="theme-color" content="@brand">
}
@if (!string.IsNullOrEmpty(Model.CssUrl))

View File

@ -21,7 +21,7 @@
<div class="col-xxl-constrain col-xl-8">
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All"></div>
<div asp-validation-summary="All" class="@(ViewContext.ModelState.ErrorCount.Equals(1) ? "no-marker" : "")"></div>
}
<div class="form-group">
<label asp-for="Id" class="form-label"></label>
@ -38,15 +38,29 @@
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div>
<h3 class="mt-5 mb-3" text-translate="true">Branding</h3>
<div class="form-group">
<h3 class="mt-5 mb-3" text-translate="true">Branding</h3>
<p>
The custom color, logo and CSS are applied on the public/customer-facing pages (Invoice, Payment Request, Pull Payment, etc.).
The brand color is used as the accent color for buttons, links, etc. It might get adapted to fit the light/dark color scheme.
</p>
<div>
<label asp-for="BrandColor" class="form-label"></label>
<div class="input-group">
<input id="BrandColorInput" class="form-control form-control-color flex-grow-0" type="color" style="width:3rem" aria-describedby="BrandColorValue" value="@Model.BrandColor" />
<input asp-for="BrandColor" class="form-control form-control-color flex-grow-0 font-monospace" pattern="@ColorPalette.Pattern" style="width:5.5rem;font-size:0.9rem" />
<div class="d-flex flex-wrap">
<div class="form-group me-4">
<div class="input-group">
<input id="BrandColorInput" class="form-control form-control-color flex-grow-0" type="color" style="width:3rem" aria-describedby="BrandColorValue" value="@Model.BrandColor" />
<input asp-for="BrandColor" class="form-control form-control-color flex-grow-0 font-monospace" pattern="@ColorPalette.Pattern" style="width:5.5rem;font-size:0.9rem" />
</div>
<span asp-validation-for="BrandColor" class="text-danger"></span>
</div>
<div class="form-group d-flex align-items-center">
<input asp-for="ApplyBrandColorToBackend" type="checkbox" class="btcpay-toggle me-3" disabled="@(string.IsNullOrEmpty(Model.BrandColor))"/>
<label asp-for="ApplyBrandColorToBackend" class="form-check-label"></label>
</div>
</div>
<span asp-validation-for="BrandColor" class="text-danger"></span>
</div>
<div class="form-group">
<div class="d-flex align-items-center justify-content-between gap-2">
<label asp-for="LogoFile" class="form-label"></label>
@ -94,9 +108,6 @@
}
</div>
<span asp-validation-for="LogoFile" class="text-danger"></span>
<div class="form-text">
Use this CSS to customize the public/customer-facing pages of this store. (Invoice, Payment Request, Pull Payment, etc.)
</div>
}
else
{
@ -193,13 +204,17 @@
(() => {
const $colorValue = document.getElementById('BrandColor');
const $colorInput = document.getElementById('BrandColorInput');
const $applyToBackend = document.getElementById('ApplyBrandColorToBackend');
delegate('change', '#BrandColor', e => {
const value = e.target.value;
if (value.match(@Safe.Json(@ColorPalette.Pattern)))
if (value.match(@Safe.Json(ColorPalette.Pattern)))
$colorInput.value = value;
$applyToBackend.disabled = !value;
});
delegate('change', '#BrandColorInput', e => {
$colorValue.value = e.target.value;
const value = e.target.value;
$colorValue.value = value;
$applyToBackend.disabled = !value;
});
})();
</script>

View File

@ -12840,7 +12840,7 @@ input[type='number'].hide-number-spin {
cursor: pointer;
}
.btcpay-toggle:hover {
.btcpay-toggle:not(:disabled):hover {
background: var(--btcpay-toggle-bg-hover);
}
@ -12855,8 +12855,8 @@ input.btcpay-toggle:checked,
background: var(--btcpay-toggle-bg-active);
}
input.btcpay-toggle:checked:hover,
.btcpay-toggle.btcpay-toggle--active:hover {
input.btcpay-toggle:not(:disabled):checked:hover,
.btcpay-toggle.btcpay-toggle--active:not(:disabled):hover {
background: var(--btcpay-toggle-bg-active-hover);
}

View File

@ -1,6 +1,6 @@
/* Variables */
:root {
--chart-main-rgb: 68, 164, 49;
--chart-main-rgb: var(--btcpay-primary-rgb);
--chart-series-a-rgb: var(--chart-main-rgb);
--chart-series-b-rgb: 245, 0, 0;
--chart-series-c-rgb: 0, 109, 242;
@ -186,8 +186,8 @@ h2 .icon.icon-info {
top: -.0125em;
}
#descriptor p {
max-width: 40em;
#mainContent section p {
max-width: 46em;
}
/* Invoices */

View File

@ -239,9 +239,9 @@
--btcpay-form-shadow-invalid: var(--btcpay-danger-shadow);
--btcpay-toggle-bg: var(--btcpay-neutral-500);
--btcpay-toggle-bg-hover: var(--btcpay-neutral-600);
--btcpay-toggle-bg-hover: var(--btcpay-primary-bg-hover);
--btcpay-toggle-bg-active: var(--btcpay-primary);
--btcpay-toggle-bg-active-hover: var(--btcpay-primary-600);
--btcpay-toggle-bg-active-hover: var(--btcpay-primary-bg-active);
--btcpay-footer-bg: var(--btcpay-body-bg);
--btcpay-footer-text: var(--btcpay-body-text-muted);

View File

@ -509,6 +509,11 @@
"nullable": true,
"example": "#F7931A"
},
"applyBrandColorToBackend": {
"type": "boolean",
"default": false,
"description": "Apply the brand color to the store's backend as well"
},
"defaultCurrency": {
"type": "string",
"description": "The default currency of the store",