From f4c886142a06125f05806a539f9db5e086b76559 Mon Sep 17 00:00:00 2001 From: "thgO.O" Date: Wed, 20 May 2026 22:42:28 -0300 Subject: [PATCH 1/5] feat: add embedded plugin directory mode --- PluginBuilder/Views/Home/AllPlugins.cshtml | 100 +++++++-- .../Views/Shared/_LayoutPublicModal.cshtml | 12 +- .../Views/Shared/_PublicHeader.cshtml | 68 ++++--- PluginBuilder/wwwroot/js/public-embed.js | 191 ++++++++++++++++++ 4 files changed, 326 insertions(+), 45 deletions(-) create mode 100644 PluginBuilder/wwwroot/js/public-embed.js diff --git a/PluginBuilder/Views/Home/AllPlugins.cshtml b/PluginBuilder/Views/Home/AllPlugins.cshtml index dffb8ed6..1dba9234 100644 --- a/PluginBuilder/Views/Home/AllPlugins.cshtml +++ b/PluginBuilder/Views/Home/AllPlugins.cshtml @@ -6,6 +6,8 @@ const string title = "BTCPay Server Plugin Directory"; const string desc = "Extend and customize your BTCPay Server with community and official plugins."; ViewData["Title"] = title; + var embedMode = Context.Request.Query["embed"] == "1"; + var embedRoute = embedMode ? "1" : null; var currentSearch = Context.Request.Query["searchPluginName"].ToString(); var currentSort = (string?)Context.Request.Query["sort"] ?? "smart"; @@ -18,6 +20,7 @@ "alpha" => "Alphabetical", _ => "Relevance" }; + var containerClass = embedMode ? "container-fluid px-0 plugin-directory-embed" : "container"; } @section Meta { @@ -35,17 +38,72 @@ -
-
-
-

BTCPay Server Plugin Directory

-

Extend and customize your BTCPay Server with community and official plugins.

+@if (embedMode) +{ + +} + +
+ @if (!embedMode) + { +
+
+

BTCPay Server Plugin Directory

+

Extend and customize your BTCPay Server with community and official plugins.

+
-
-
+ } +
+ @if (embedMode) + { + + } Relevance Highest rated Most recent Alphabetical @@ -92,14 +154,15 @@
-
+
@if (Model != null && Model.Any()) { @foreach (var plugin in Model) { var owner = plugin.GetOwnerName(GitHostingProviderFactory); var ownerProfileUrl = plugin.GetOwnerProfileUrl(GitHostingProviderFactory); -
+ var pluginIdentifier = plugin.ManifestInfo?["Identifier"]?.ToString(); +
@if (plugin.IsUnlisted) @@ -116,6 +179,9 @@

@plugin.PluginTitle

@@ -159,9 +225,19 @@ {

No plugins available yet

-

Check back later, or - login to upload one -

+ @if (!embedMode) + { +

Check back later, or + login to upload one +

+ } +
+ } + + @if (embedMode && Model != null && Model.Any()) + { + }
diff --git a/PluginBuilder/Views/Shared/_LayoutPublicModal.cshtml b/PluginBuilder/Views/Shared/_LayoutPublicModal.cshtml index c92341b0..fdaca09a 100644 --- a/PluginBuilder/Views/Shared/_LayoutPublicModal.cshtml +++ b/PluginBuilder/Views/Shared/_LayoutPublicModal.cshtml @@ -1,5 +1,6 @@ @{ Layout = null; + var embedMode = string.Equals(Context.Request.Query["embed"], "1", StringComparison.Ordinal); } @@ -24,14 +25,23 @@ .account-form h4 { margin-bottom: 1.5rem; } + + body.embed-mode { + background: transparent; + overflow-x: hidden; + } - +
@RenderBody()
+@if (embedMode) +{ + +} diff --git a/PluginBuilder/Views/Shared/_PublicHeader.cshtml b/PluginBuilder/Views/Shared/_PublicHeader.cshtml index edf8b1d2..8ba5f732 100644 --- a/PluginBuilder/Views/Shared/_PublicHeader.cshtml +++ b/PluginBuilder/Views/Shared/_PublicHeader.cshtml @@ -1,39 +1,43 @@ @{ var isAuth = User.Identity?.IsAuthenticated == true; + var embedMode = string.Equals(Context.Request.Query["embed"], "1", StringComparison.Ordinal); } -
-
- +@if (!embedMode) +{ +
+
+ -
-
-
+} diff --git a/PluginBuilder/wwwroot/js/public-embed.js b/PluginBuilder/wwwroot/js/public-embed.js new file mode 100644 index 00000000..f3b381d1 --- /dev/null +++ b/PluginBuilder/wwwroot/js/public-embed.js @@ -0,0 +1,191 @@ +(function () { + const embedPage = document.querySelector("[data-embed-page]"); + if (!embedPage || window.parent === window) { + return; + } + + let lastHeight = 0; + let heightPostQueued = false; + let hiddenPluginIdentifiers = new Set(); + + function applyHostColorMode(colorMode) { + if (colorMode !== "light" && colorMode !== "dark") { + return; + } + + document.documentElement.setAttribute("data-btcpay-theme", colorMode); + + const darkThemeLink = document.getElementById("DarkThemeLinkTag"); + if (darkThemeLink) { + darkThemeLink.disabled = colorMode !== "dark"; + } + + scheduleHeightPost(); + } + + function postReady() { + window.parent.postMessage({ type: "pb:ready" }, "*"); + } + + function getContentHeight() { + const rectHeight = embedPage.getBoundingClientRect ? embedPage.getBoundingClientRect().height : 0; + return Math.max( + embedPage.scrollHeight || 0, + embedPage.offsetHeight || 0, + Math.ceil(rectHeight) + ); + } + + function postHeight() { + heightPostQueued = false; + const height = Math.ceil(getContentHeight()) + 4; + if (!height || Math.abs(height - lastHeight) < 2) { + return; + } + + lastHeight = height; + window.parent.postMessage({ + type: "pb:content-height", + height: height + }, "*"); + } + + function scheduleHeightPost() { + if (heightPostQueued) { + return; + } + + heightPostQueued = true; + window.requestAnimationFrame(postHeight); + } + + function normalizeIdentifier(identifier) { + return typeof identifier === "string" ? identifier.trim().toLowerCase() : ""; + } + + function normalizeHiddenPluginIdentifiers(value) { + const identifiers = new Set(); + if (!Array.isArray(value)) { + return identifiers; + } + + value.forEach(function (identifier) { + const normalizedIdentifier = normalizeIdentifier(identifier); + if (normalizedIdentifier) { + identifiers.add(normalizedIdentifier); + } + }); + + return identifiers; + } + + function applyHiddenPluginFilter() { + if (embedPage.dataset.embedPage !== "list") { + return; + } + + let visibleCount = 0; + document.querySelectorAll("[data-plugin-card]").forEach(function (card) { + const identifier = normalizeIdentifier(card.dataset.pluginIdentifier); + const shouldHide = identifier && hiddenPluginIdentifiers.has(identifier); + card.hidden = shouldHide; + if (!shouldHide) { + visibleCount += 1; + } + }); + + const emptyState = document.querySelector("[data-plugin-directory-empty-state]"); + if (emptyState) { + emptyState.hidden = visibleCount !== 0; + } + + scheduleHeightPost(); + } + + function postSelection(slug, identifier) { + if (!slug) { + return; + } + + window.parent.postMessage({ + type: "pb:plugin-selected", + slug: slug, + identifier: identifier || null + }, "*"); + } + + function buildDetailsUrl(slug) { + const url = new URL("/public/plugins/" + encodeURIComponent(slug), window.location.origin); + url.searchParams.set("embed", "1"); + return url.toString(); + } + + function handleHostContext(event) { + const data = event.data; + if (!data || typeof data !== "object" || data.type !== "btcpay:host-context") { + return; + } + + hiddenPluginIdentifiers = normalizeHiddenPluginIdentifiers(data.hiddenPluginIdentifiers); + applyHostColorMode(data.colorMode); + applyHiddenPluginFilter(); + + const currentSlug = embedPage.dataset.pluginSlug || ""; + const selectedSlug = typeof data.selectedSlug === "string" ? data.selectedSlug : ""; + + if (embedPage.dataset.embedPage === "list") { + return; + } + + if (!selectedSlug || selectedSlug === currentSlug) { + return; + } + + window.location.replace(buildDetailsUrl(selectedSlug)); + } + + postReady(); + scheduleHeightPost(); + + if (embedPage.dataset.embedPage === "details") { + postSelection(embedPage.dataset.pluginSlug || "", embedPage.dataset.pluginIdentifier || ""); + } + + document.querySelectorAll("img").forEach(function (image) { + image.addEventListener("load", scheduleHeightPost); + image.addEventListener("error", scheduleHeightPost); + }); + + document.querySelectorAll("a[data-plugin-slug]").forEach(function (link) { + link.addEventListener("click", function (event) { + if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0) { + return; + } + + event.preventDefault(); + postSelection(link.dataset.pluginSlug || "", link.dataset.pluginIdentifier || ""); + }); + }); + + window.addEventListener("message", handleHostContext); + window.addEventListener("load", scheduleHeightPost); + window.addEventListener("resize", scheduleHeightPost); + + if (window.ResizeObserver) { + const resizeObserver = new window.ResizeObserver(scheduleHeightPost); + resizeObserver.observe(embedPage); + } + + if (window.MutationObserver) { + const mutationObserver = new window.MutationObserver(scheduleHeightPost); + mutationObserver.observe(embedPage, { childList: true, subtree: true, attributes: true }); + } + + if (document.fonts && document.fonts.ready) { + document.fonts.ready.then(scheduleHeightPost); + } + + window.setTimeout(scheduleHeightPost, 0); + window.setTimeout(scheduleHeightPost, 150); + window.setTimeout(scheduleHeightPost, 500); +})(); From 47a1fa335230b540fdcb59bff31f0e6f37e35d45 Mon Sep 17 00:00:00 2001 From: "thgO.O" Date: Wed, 20 May 2026 22:42:45 -0300 Subject: [PATCH 2/5] feat: support embedded plugin details --- PluginBuilder/Controllers/HomeController.cs | 32 +++++++--- .../ViewModels/Home/PluginDetailsViewModel.cs | 1 + .../Views/Home/GetPluginDetails.cshtml | 58 ++++++++++++++----- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/PluginBuilder/Controllers/HomeController.cs b/PluginBuilder/Controllers/HomeController.cs index 994a0f27..380ce5ab 100644 --- a/PluginBuilder/Controllers/HomeController.cs +++ b/PluginBuilder/Controllers/HomeController.cs @@ -509,7 +509,8 @@ ORDER BY " + orderBy + @" Contributors = pluginContributors, RatingFilter = model.RatingFilter, OwnerGithubUrl = ownerGithubUrl, - OwnerNostrUrl = ownerNostrUrl + OwnerNostrUrl = ownerNostrUrl, + EmbedMode = string.Equals(Request.Query["embed"], "1", StringComparison.Ordinal) }; return View(vm); } @@ -518,7 +519,7 @@ ORDER BY " + orderBy + @" [ValidateAntiForgeryToken] public async Task UpsertReview( [ModelBinder(typeof(PluginSlugModelBinder))] - PluginSlug pluginSlug, int rating, string? body, string? pluginVersion) + PluginSlug pluginSlug, int rating, string? body, string? pluginVersion, string? embed = null) { if (rating is < 1 or > 5) return BadRequest("Invalid rating"); @@ -533,7 +534,7 @@ public async Task UpsertReview( if (isOwner) { TempData[TempDataConstant.WarningMessage] = "You cannot review your own plugin."; - return RedirectToAction(nameof(GetPluginDetails), "Home", new { pluginSlug = pluginSlug.ToString() }); + return RedirectToAction(nameof(GetPluginDetails), "Home", new { pluginSlug = pluginSlug.ToString(), embed = string.IsNullOrEmpty(embed) ? null : embed }); } var reviewerAccountDetails = await conn.GetAccountDetailSettings(userId) ?? new AccountSettings(); @@ -558,7 +559,12 @@ public async Task UpsertReview( await conn.UpsertPluginReview(reviewViewModel); var sort = Request.Query["sort"].ToString(); - var url = Url.Action(nameof(GetPluginDetails), "Home", new { pluginSlug = pluginSlug.ToString(), sort = string.IsNullOrEmpty(sort) ? null : sort }); + var url = Url.Action(nameof(GetPluginDetails), "Home", new + { + pluginSlug = pluginSlug.ToString(), + sort = string.IsNullOrEmpty(sort) ? null : sort, + embed = string.IsNullOrEmpty(embed) ? null : embed + }); return Redirect((url ?? "/") + "#reviews"); } @@ -618,7 +624,8 @@ public async Task VoteReview( [ModelBinder(typeof(PluginSlugModelBinder))] PluginSlug pluginSlug, long id, - bool isHelpful) + bool isHelpful, + string? embed = null) { var userId = userManager.GetUserId(User); if (string.IsNullOrEmpty(userId)) @@ -635,7 +642,11 @@ public async Task VoteReview( if (!ok) TempData[TempDataConstant.WarningMessage] = "Error while updating review helpful vote"; - var url = Url.Action(nameof(GetPluginDetails), new { pluginSlug = pluginSlug.ToString() }); + var url = Url.Action(nameof(GetPluginDetails), new + { + pluginSlug = pluginSlug.ToString(), + embed = string.IsNullOrEmpty(embed) ? null : embed + }); return Redirect((url ?? "/") + "#reviews"); } @@ -644,7 +655,8 @@ public async Task VoteReview( public async Task DeleteReview( [ModelBinder(typeof(PluginSlugModelBinder))] PluginSlug pluginSlug, - long id) + long id, + string? embed = null) { var userId = userManager.GetUserId(User); if (string.IsNullOrEmpty(userId)) @@ -659,7 +671,11 @@ public async Task DeleteReview( if (!ok) TempData[TempDataConstant.WarningMessage] = "Error while deleting review"; - var url = Url.Action(nameof(GetPluginDetails), new { pluginSlug = pluginSlug.ToString() }); + var url = Url.Action(nameof(GetPluginDetails), new + { + pluginSlug = pluginSlug.ToString(), + embed = string.IsNullOrEmpty(embed) ? null : embed + }); return Redirect((url ?? "/") + "#reviews"); } diff --git a/PluginBuilder/ViewModels/Home/PluginDetailsViewModel.cs b/PluginBuilder/ViewModels/Home/PluginDetailsViewModel.cs index 654c47c9..a0b1e0f9 100644 --- a/PluginBuilder/ViewModels/Home/PluginDetailsViewModel.cs +++ b/PluginBuilder/ViewModels/Home/PluginDetailsViewModel.cs @@ -24,6 +24,7 @@ public PluginDetailsViewModel() public string? OwnerGithubUrl { get; set; } public string? OwnerNostrUrl { get; set; } + public bool EmbedMode { get; set; } } public class Review diff --git a/PluginBuilder/Views/Home/GetPluginDetails.cshtml b/PluginBuilder/Views/Home/GetPluginDetails.cshtml index 1da07abc..a0c0b3d7 100644 --- a/PluginBuilder/Views/Home/GetPluginDetails.cshtml +++ b/PluginBuilder/Views/Home/GetPluginDetails.cshtml @@ -7,11 +7,17 @@ Layout = "_LayoutPublicModal"; var desc = string.IsNullOrWhiteSpace(Model.Plugin.Description) ? "Plugin for BTCPay Server" : Model.Plugin.Description; ViewData["Title"] = Model.Plugin.PluginTitle + " - " + desc; + var embedRoute = Model.EmbedMode ? "1" : null; + var pluginIdentifier = Model.Plugin.ManifestInfo?["Identifier"]?.ToString(); var owner = Model.Plugin.GetGithubRepository()?.Owner; var dependencies = Model.Plugin.ManifestInfo?["Dependencies"] as JArray; var sourceUrl = Model.Plugin.GetGithubRepository()?.GetSourceUrl(Model.Plugin.BuildInfo?["gitCommit"]?.ToString(), Model.Plugin.BuildInfo?["pluginDir"]?.ToString()); var pluginUrl = Url.Action(nameof(HomeController.GetPluginDetails), "Home", new { pluginSlug = Model.Plugin.ProjectSlug }, Context.Request.Scheme, Context.Request.Host.ToString()); + var writeReviewUrl = Model.EmbedMode ? $"{pluginUrl}#write-review" : "#write-review"; + var writeReviewTarget = Model.EmbedMode ? "_blank" : null; + var writeReviewRel = Model.EmbedMode ? "noopener noreferrer" : null; var currentRating = Model.RatingFilter; + var containerClass = Model.EmbedMode ? "container-fluid px-0 plugin-directory-embed" : "container"; DateTimeOffset.TryParse(Model.Plugin.BuildInfo?["buildDate"]?.ToString(), out var buildDate); var videoEmbedUrl = Model.Plugin.VideoUrl.GetVideoEmbedUrl(); @@ -45,6 +51,16 @@ +@if (Model.EmbedMode) +{ + +} + @if (Model.ShowHiddenNotice) { } -
+
@@ -168,7 +187,7 @@
@@ -180,6 +199,7 @@ @@ -223,7 +245,7 @@ @if (!Model.Reviews.Any()) {
- No reviews yet. Be the first to write one. + No reviews yet. Be the first to write one.
} else @@ -278,13 +300,13 @@ } else { -
+ @Html.AntiForgeryToken()
-
+ @Html.AntiForgeryToken()
@@ -440,11 +465,14 @@
- Download + @if (!Model.EmbedMode) + { + Download + }
From 092ff6b0a3ca1caddfc5ca827e995fda28a3e9b1 Mon Sep 17 00:00:00 2001 From: "thgO.O" Date: Wed, 20 May 2026 23:27:35 -0300 Subject: [PATCH 3/5] fix: restrict embed host messages to parent --- PluginBuilder/wwwroot/js/public-embed.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/PluginBuilder/wwwroot/js/public-embed.js b/PluginBuilder/wwwroot/js/public-embed.js index f3b381d1..96dc2339 100644 --- a/PluginBuilder/wwwroot/js/public-embed.js +++ b/PluginBuilder/wwwroot/js/public-embed.js @@ -121,6 +121,10 @@ } function handleHostContext(event) { + if (event.source !== window.parent) { + return; + } + const data = event.data; if (!data || typeof data !== "object" || data.type !== "btcpay:host-context") { return; From 1dbdf20d77db44da5b75603b3551339be99c19f4 Mon Sep 17 00:00:00 2001 From: "thgO.O" Date: Wed, 20 May 2026 23:33:18 -0300 Subject: [PATCH 4/5] refactor: simplify embed height updates --- PluginBuilder/wwwroot/js/public-embed.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/PluginBuilder/wwwroot/js/public-embed.js b/PluginBuilder/wwwroot/js/public-embed.js index 96dc2339..d6eaf1e4 100644 --- a/PluginBuilder/wwwroot/js/public-embed.js +++ b/PluginBuilder/wwwroot/js/public-embed.js @@ -172,7 +172,6 @@ }); window.addEventListener("message", handleHostContext); - window.addEventListener("load", scheduleHeightPost); window.addEventListener("resize", scheduleHeightPost); if (window.ResizeObserver) { @@ -180,16 +179,7 @@ resizeObserver.observe(embedPage); } - if (window.MutationObserver) { - const mutationObserver = new window.MutationObserver(scheduleHeightPost); - mutationObserver.observe(embedPage, { childList: true, subtree: true, attributes: true }); - } - if (document.fonts && document.fonts.ready) { document.fonts.ready.then(scheduleHeightPost); } - - window.setTimeout(scheduleHeightPost, 0); - window.setTimeout(scheduleHeightPost, 150); - window.setTimeout(scheduleHeightPost, 500); })(); From e60d418fa35ac0e27102a6ae5c4b71dc474e4561 Mon Sep 17 00:00:00 2001 From: "thgO.O" Date: Wed, 20 May 2026 23:52:19 -0300 Subject: [PATCH 5/5] refactor: use bootstrap classes for embed layout --- PluginBuilder/Views/Home/AllPlugins.cshtml | 61 +++---------------- .../Views/Home/GetPluginDetails.cshtml | 12 +--- 2 files changed, 8 insertions(+), 65 deletions(-) diff --git a/PluginBuilder/Views/Home/AllPlugins.cshtml b/PluginBuilder/Views/Home/AllPlugins.cshtml index 1dba9234..abe41e96 100644 --- a/PluginBuilder/Views/Home/AllPlugins.cshtml +++ b/PluginBuilder/Views/Home/AllPlugins.cshtml @@ -20,7 +20,6 @@ "alpha" => "Alphabetical", _ => "Relevance" }; - var containerClass = embedMode ? "container-fluid px-0 plugin-directory-embed" : "container"; } @section Meta { @@ -38,55 +37,7 @@ -@if (embedMode) -{ - -} - -
+
@if (!embedMode) {
@@ -96,8 +47,8 @@
} -
-
+
+
@if (embedMode) @@ -154,7 +105,8 @@
-
+
+
@if (Model != null && Model.Any()) { @foreach (var plugin in Model) @@ -162,7 +114,7 @@ var owner = plugin.GetOwnerName(GitHostingProviderFactory); var ownerProfileUrl = plugin.GetOwnerProfileUrl(GitHostingProviderFactory); var pluginIdentifier = plugin.ManifestInfo?["Identifier"]?.ToString(); -
+
@if (plugin.IsUnlisted) @@ -240,5 +192,6 @@

All compatible plugins from the public directory are already installed or disabled on this server.

} +
diff --git a/PluginBuilder/Views/Home/GetPluginDetails.cshtml b/PluginBuilder/Views/Home/GetPluginDetails.cshtml index a0c0b3d7..9f8945fe 100644 --- a/PluginBuilder/Views/Home/GetPluginDetails.cshtml +++ b/PluginBuilder/Views/Home/GetPluginDetails.cshtml @@ -17,7 +17,7 @@ var writeReviewTarget = Model.EmbedMode ? "_blank" : null; var writeReviewRel = Model.EmbedMode ? "noopener noreferrer" : null; var currentRating = Model.RatingFilter; - var containerClass = Model.EmbedMode ? "container-fluid px-0 plugin-directory-embed" : "container"; + var containerClass = Model.EmbedMode ? "container-fluid px-0" : "container"; DateTimeOffset.TryParse(Model.Plugin.BuildInfo?["buildDate"]?.ToString(), out var buildDate); var videoEmbedUrl = Model.Plugin.VideoUrl.GetVideoEmbedUrl(); @@ -51,16 +51,6 @@ -@if (Model.EmbedMode) -{ - -} - @if (Model.ShowHiddenNotice) {