mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Store Branding: Add custom CSS option
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
26
BTCPayServer/Views/Shared/LayoutHeadStoreBranding.cshtml
Normal file
26
BTCPayServer/Views/Shared/LayoutHeadStoreBranding.cshtml
Normal 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" />
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user