Store Branding: Add custom CSS option

This commit is contained in:
Dennis Reimann
2022-12-19 15:51:05 +01:00
parent f10c1c4730
commit 4df2f1f756
11 changed files with 158 additions and 64 deletions

View File

@@ -166,17 +166,28 @@ namespace BTCPayServer.Controllers
if (receipt.Enabled is not true)
return NotFound();
if (i.Status.ToModernStatus() != InvoiceStatus.Settled)
{
return View(new InvoiceReceiptViewModel
var storeBlob = store.GetStoreBlob();
var vm = new InvoiceReceiptViewModel
{
InvoiceId = i.Id,
OrderId = i.Metadata?.OrderId,
OrderUrl = i.Metadata?.OrderUrl,
Status = i.Status.ToModernStatus(),
Currency = i.Currency,
Timestamp = i.InvoiceTime,
StoreName = store.StoreName,
Status = i.Status.ToModernStatus()
});
BrandColor = storeBlob.BrandColor,
LogoFileId = storeBlob.LogoFileId,
CssFileId = storeBlob.CssFileId,
ReceiptOptions = receipt
};
if (i.Status.ToModernStatus() != InvoiceStatus.Settled)
{
return View(vm);
}
JToken? receiptData = null;
i.Metadata?.AdditionalData?.TryGetValue("receiptData", out receiptData);
@@ -213,23 +224,13 @@ namespace BTCPayServer.Controllers
.Where(payment => payment != null)
.ToList();
return View(new InvoiceReceiptViewModel
{
StoreName = store.StoreName,
StoreLogoFileId = store.GetStoreBlob().LogoFileId,
Status = i.Status.ToModernStatus(),
Amount = payments.Sum(p => p!.Paid),
Currency = i.Currency,
Timestamp = i.InvoiceTime,
InvoiceId = i.Id,
OrderId = i.Metadata?.OrderId,
OrderUrl = i.Metadata?.OrderUrl,
Payments = receipt.ShowPayments is false ? null : payments,
ReceiptOptions = receipt,
AdditionalData = receiptData is null
vm.Amount = payments.Sum(p => p!.Paid);
vm.Payments = receipt.ShowPayments is false ? null : payments;
vm.AdditionalData = receiptData is null
? new Dictionary<string, object>()
: PosDataParser.ParsePosData(receiptData.ToString())
});
: PosDataParser.ParsePosData(receiptData.ToString());
return View(vm);
}
private string? GetTransactionLink(PaymentMethodId paymentMethodId, string txId)
{
@@ -762,6 +763,7 @@ namespace BTCPayServer.Controllers
CustomCSSLink = storeBlob.CustomCSS,
CustomLogoLink = storeBlob.CustomLogo,
LogoFileId = storeBlob.LogoFileId,
CssFileId = storeBlob.CssFileId,
BrandColor = storeBlob.BrandColor,
CheckoutType = invoice.CheckoutType ?? storeBlob.CheckoutType,
HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice",

View File

@@ -608,6 +608,7 @@ namespace BTCPayServer.Controllers
StoreName = store.StoreName,
StoreWebsite = store.StoreWebsite,
LogoFileId = storeBlob.LogoFileId,
CssFileId = storeBlob.CssFileId,
BrandColor = storeBlob.BrandColor,
NetworkFeeMode = storeBlob.NetworkFeeMode,
AnyoneCanCreateInvoice = storeBlob.AnyoneCanInvoice,
@@ -622,7 +623,10 @@ namespace BTCPayServer.Controllers
}
[HttpPost("{storeId}/settings")]
public async Task<IActionResult> GeneralSettings(GeneralSettingsViewModel model, [FromForm] bool RemoveLogoFile = false)
public async Task<IActionResult> GeneralSettings(
GeneralSettingsViewModel model,
[FromForm] bool RemoveLogoFile = false,
[FromForm] bool RemoveCssFile = false)
{
bool needUpdate = false;
if (CurrentStore.StoreName != model.StoreName)
@@ -688,6 +692,39 @@ namespace BTCPayServer.Controllers
needUpdate = true;
}
if (model.CssFile != null)
{
if (model.CssFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
{
// delete existing CSS file
if (!string.IsNullOrEmpty(blob.CssFileId))
{
await _fileService.RemoveFile(blob.CssFileId, userId);
}
// add new CSS file
try
{
var storedFile = await _fileService.AddFile(model.CssFile, userId);
blob.CssFileId = storedFile.Id;
}
catch (Exception e)
{
TempData[WellKnownTempData.ErrorMessage] = $"Could not save CSS file: {e.Message}";
}
}
else
{
TempData[WellKnownTempData.ErrorMessage] = "The uploaded file needs to be a CSS file";
}
}
else if (RemoveCssFile && !string.IsNullOrEmpty(blob.CssFileId))
{
await _fileService.RemoveFile(blob.CssFileId, userId);
blob.CssFileId = null;
needUpdate = true;
}
if (CurrentStore.SetStoreBlob(blob))
{
needUpdate = true;

View File

@@ -215,8 +215,9 @@ namespace BTCPayServer.Data
public TimeSpan RefundBOLT11Expiration { get; set; }
public List<UIStoresController.StoreEmailRule> EmailRules { get; set; }
public string LogoFileId { get; set; }
public string BrandColor { get; set; }
public string LogoFileId { get; set; }
public string CssFileId { get; set; }
public IPaymentFilter GetExcludedPaymentMethods()
{

View File

@@ -13,7 +13,9 @@ namespace BTCPayServer.Models.InvoicingModels
public string OrderId { get; set; }
public string Currency { get; set; }
public string StoreName { get; set; }
public string StoreLogoFileId { get; set; }
public string BrandColor { get; set; }
public string LogoFileId { get; set; }
public string CssFileId { get; set; }
public decimal Amount { get; set; }
public DateTimeOffset Timestamp { get; set; }
public Dictionary<string, object> AdditionalData { get; set; }

View File

@@ -24,6 +24,7 @@ namespace BTCPayServer.Models.InvoicingModels
}
public string CustomCSSLink { get; set; }
public string CustomLogoLink { get; set; }
public string CssFileId { get; set; }
public string LogoFileId { get; set; }
public string BrandColor { get; set; }
public string HtmlTitle { get; set; }

View File

@@ -22,12 +22,16 @@ namespace BTCPayServer.Models.StoreViewModels
[MaxLength(500)]
public string StoreWebsite { get; set; }
[Display(Name = "Brand Color")]
public string BrandColor { get; set; }
[Display(Name = "Logo")]
public IFormFile LogoFile { get; set; }
public string LogoFileId { get; set; }
[Display(Name = "Brand Color")]
public string BrandColor { get; set; }
[Display(Name = "Custom CSS")]
public IFormFile CssFile { get; set; }
public string CssFileId { get; set; }
public bool CanDelete { get; set; }

View File

@@ -0,0 +1,26 @@
@model (string BrandColor, string CssFileId)
@using BTCPayServer.Abstractions.Extensions
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@inject IFileService FileService
@{
var cssUrl = !string.IsNullOrEmpty(Model.CssFileId)
? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CssFileId)
: null;
}
@if (!string.IsNullOrEmpty(Model.BrandColor))
{
<style>
:root {
--btcpay-primary: @Model.BrandColor;
--btcpay-primary-bg-hover: @Model.BrandColor;
--btcpay-primary-bg-active: @Model.BrandColor;
--btcpay-primary-shadow: @Model.BrandColor;
--btcpay-body-link-accent: @Model.BrandColor;
}
</style>
}
@if (!string.IsNullOrEmpty(cssUrl))
{
<link href="@cssUrl" asp-append-version="true" rel="stylesheet" />
}

View File

@@ -15,7 +15,7 @@
var paymentMethodCount = Model.AvailableCryptos.Count;
var logoUrl = !string.IsNullOrEmpty(Model.LogoFileId)
? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.LogoFileId)
: Model.CustomLogoLink;
: null;
}
@functions {
private string PaymentMethodName(PaymentModel.AvailableCrypto pm)
@@ -36,27 +36,16 @@
<partial name="LayoutHead"/>
<meta name="robots" content="noindex,nofollow">
<link href="~/checkout-v2/checkout.css" asp-append-version="true" rel="stylesheet" />
@if (!string.IsNullOrEmpty(Model.BrandColor))
{
<style>
:root {
--btcpay-primary: @Model.BrandColor;
--btcpay-primary-bg-hover: @Model.BrandColor;
--btcpay-primary-bg-active: @Model.BrandColor;
--btcpay-primary-shadow: @Model.BrandColor;
--btcpay-body-link-accent: @Model.BrandColor;
}
</style>
}
<partial name="LayoutHeadStoreBranding" model="@(Model.BrandColor, Model.CssFileId)" />
</head>
<body class="min-vh-100">
<div id="Checkout-v2" class="wrap gap-4" v-cloak v-waitForT>
<header class="store-header">
@if (!string.IsNullOrEmpty(logoUrl))
{
<img src="@logoUrl" alt="@Model.StoreName" class="logo @(!string.IsNullOrEmpty(Model.LogoFileId) ? "logo--square" : "")"/>
<img src="@logoUrl" alt="@Model.StoreName" class="store-logo"/>
}
<h1>@Model.StoreName</h1>
<h1 class="store-name">@Model.StoreName</h1>
</header>
<main class="shadow-lg">
<nav v-if="isModal">

View File

@@ -7,14 +7,15 @@
@inject CurrencyNameTable CurrencyNameTable
@using BTCPayServer.Abstractions.Contracts
@inject IFileService FileService
@{
Layout = null;
ViewData["Title"] = Model.StoreName;
ViewData["Title"] = $"Receipt from {Model.StoreName}";
var isProcessing = Model.Status == InvoiceStatus.Processing;
var isSettled = Model.Status == InvoiceStatus.Settled;
var logoUrl = !string.IsNullOrEmpty(Model.LogoFileId)
? await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.LogoFileId)
: null;
}
<!DOCTYPE html>
<html lang="en" @(env.IsDeveloping ? " data-devenv" : "")>
<head>
@@ -31,6 +32,7 @@
#posData td > table:last-child { margin-bottom: 0 !important; }
#posData table > tbody > tr:first-child > td > h4 { margin-top: 0 !important; }
</style>
<partial name="LayoutHeadStoreBranding" model="@(Model.BrandColor, Model.CssFileId)" />
</head>
<body>
<div class="min-vh-100 d-flex flex-column">
@@ -40,11 +42,11 @@
<div class="d-flex flex-column justify-content-center gap-4">
<header class="store-header">
@if (!string.IsNullOrEmpty(Model.StoreLogoFileId))
@if (!string.IsNullOrEmpty(logoUrl))
{
<img src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.StoreLogoFileId))" alt="@Model.StoreName" class="logo @(!string.IsNullOrEmpty(Model.StoreLogoFileId) ? "logo--square" : "")" />
<img src="@logoUrl" alt="@Model.StoreName" class="store-logo" />
}
<h1>@ViewData["Title"]</h1>
<h1 class="store-name">@Model.StoreName</h1>
</header>
<div id="InvoiceSummary" class="bg-tile p-3 p-sm-4 rounded d-flex flex-wrap align-items-center">
@if (isProcessing)

View File

@@ -34,6 +34,14 @@
</div>
<h3 class="mt-5 mb-3">Branding</h3>
<div class="form-group">
<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" />
</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>
@@ -66,12 +74,34 @@
}
</div>
<div class="form-group">
<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" />
<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))
{
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveCssFile" value="true">
<span class="fa fa-times"></span> Remove
</button>
}
</div>
<span asp-validation-for="BrandColor" class="text-danger"></span>
@if (canUpload)
{
<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))
{
<a href="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CssFileId))" target="_blank" rel="noreferrer noopener" class="text-nowrap">Custom CSS</a>
}
</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
{
<input asp-for="CssFile" type="file" class="form-control" disabled>
<div class="form-text">In order to upload a CSS file, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</div>
}
</div>
<h3 class="mt-5 mb-3">Payment</h3>

View File

@@ -207,24 +207,24 @@ h2 small .fa-question-circle-o {
/* Store header */
.store-header {
--logo-size: 3rem;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--btcpay-space-s);
}
.store-header .logo {
height: var(--logo-size);
}
.store-logo {
--logo-size: 3rem;
--logo-bg: transparent;
--logo-radius: 50%;
.store-header .logo--square {
width: var(--logo-size);
border-radius: 50%;
height: var(--logo-size);
background: var(--logo-bg);
border-radius: var(--logo-radius);
}
.store-header h1 {
.store-name {
font-size: 1.3rem;
}