fix(ui): show Registry subscription and setup prompts (#13038)

This commit is contained in:
Alejandro Bailo authored and GitHub committed 2026-10-14 13:51:08 +02:00
1 parent 286ae6c7cf
commit 64c19b3db6
23 files changed
+571 -73

No files matched your search

@@ -440,6 +440,28 @@ describe("RegistryExplorer", () => {
expect(cardFor("AWS guard")).toBeTruthy();
});
it("re-renders the page when a refresh reports a lapsed subscription", async () => {
// Given
const screen = await render(<RegistryExplorer initialState={readyState} />);
refreshRegistryCollectionsMock.mockResolvedValue({
status: "subscription_required",
});
// When
await screen.getByRole("button", { name: "Refresh Registry" }).click();
// Then
// The mocked router never swaps the page, so the explorer may refresh again.
await expect
.poll(() => vi.mocked(registryRouter.refresh).mock.calls.length)
.toBeGreaterThanOrEqual(1);
expect(vi.mocked(registryRouter.replace)).not.toHaveBeenCalled();
await expect
.element(screen.getByRole("button", { name: "Refresh Registry" }))
.toBeEnabled();
expect(document.body.textContent).not.toContain(
"Registry could not be refreshed.",
);
});
it.each(["access_denied", "reconnect", "onboarding"])(
"honors %s from a collection refresh",
async (status) => {
@@ -182,6 +182,9 @@ export function RegistryExplorer({
onResult: (result) => {
if (result.status === "access_denied") {
router.replace("/profile");
} else if (result.status === "subscription_required") {
// The server page swaps the catalog for the paid-plan prompt.
router.refresh();
} else if (result.status === "complete") {
setRefreshMessage(undefined);
setState((current) =>
@@ -0,0 +1,63 @@
import { afterEach, describe, expect, vi } from "vitest";
import { page } from "vitest/browser";
import { it } from "@/__tests__/fixtures";
import { render } from "@/__tests__/render-browser";
import { CloudUpgradeModal } from "@/components/shared/cloud-upgrade-modal";
import { useCloudUpgradeStore } from "@/store/cloud-upgrade/store";
import { RegistryUpgradePrompt } from "./registry-upgrade-prompt";
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({ permissions: { manage_billing: true } }),
}));
const VIEWPORTS = [
{ name: "mobile", width: 390, height: 844 },
{ name: "tablet", width: 768, height: 1024 },
{ name: "desktop", width: 1280, height: 800 },
];
describe("Registry paid plan prompt", () => {
afterEach(() => {
useCloudUpgradeStore.getState().closeCloudUpgrade();
});
it.each(VIEWPORTS)(
"opens the shared paid plan modal on $name",
async ({ name, width, height }) => {
// Given
await page.viewport(width, height);
const screen = await render(
<main className="bg-bg-neutral-primary min-h-screen p-6">
<RegistryUpgradePrompt />
<CloudUpgradeModal />
</main>,
);
// When
await screen.getByRole("button", { name: "Unlock Registry" }).click();
// Then
await expect
.element(page.getByRole("dialog", { name: "Unlock Prowler Registry" }))
.toBeVisible();
await expect
.element(page.getByRole("link", { name: "Upgrade to Unlock Registry" }))
.toHaveAttribute("href", "/billing?feature=registry");
expect(document.documentElement.scrollWidth).toBeLessThanOrEqual(width);
await expect
.poll(() =>
page
.getByRole("dialog", { name: "Unlock Prowler Registry" })
.element()
.getAnimations({ subtree: true })
.every((animation) => animation.playState !== "running"),
)
.toBe(true);
await page.screenshot({
path: `../../tests/providers/evidence/registry-paywall-${name}.png`,
});
},
);
});
@@ -0,0 +1,46 @@
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { RegistrySetupPrompt } from "./registry-upgrade-prompt";
const { permissions } = vi.hoisted(() => ({
permissions: { manage_registry: false },
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({ permissions }),
}));
describe("RegistrySetupPrompt", () => {
beforeEach(() => {
permissions.manage_registry = false;
});
it("sends Registry managers to connect their key", () => {
// Given
permissions.manage_registry = true;
// When
render(<RegistrySetupPrompt />);
// Then
expect(
screen.getByRole("link", { name: "Connect Registry" }),
).toHaveAttribute("href", "/registry");
});
it("asks everyone else for an administrator", () => {
// When
render(<RegistrySetupPrompt />);
// Then
expect(
screen.getByText(
"Ask an administrator to connect Registry before adding custom providers.",
),
).toBeVisible();
expect(
screen.queryByRole("link", { name: "Connect Registry" }),
).not.toBeInTheDocument();
});
});
@@ -0,0 +1,49 @@
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/shadcn/alert";
import { Button } from "@/components/shadcn/button/button";
import { useAuth } from "@/hooks/use-auth";
import { useCloudUpgradeStore } from "@/store/cloud-upgrade/store";
import { PAID_PLAN_UPGRADE_FEATURE } from "@/types/cloud-upgrade";
export function RegistryUpgradePrompt() {
const openCloudUpgrade = useCloudUpgradeStore(
(state) => state.openCloudUpgrade,
);
return (
<Alert>
<AlertTitle>Registry is available on paid plans</AlertTitle>
<AlertDescription>
Unlock custom providers, security checks, and compliance frameworks.
<Button
type="button"
onClick={() => openCloudUpgrade(PAID_PLAN_UPGRADE_FEATURE.REGISTRY)}
>
Unlock Registry
</Button>
</AlertDescription>
</Alert>
);
}
export function RegistrySetupPrompt() {
const { permissions } = useAuth();
return (
<Alert>
<AlertTitle>Registry is not connected yet</AlertTitle>
<AlertDescription>
{permissions.manage_registry === true ? (
<>
Connect your Registry API key to load custom providers.
<Button asChild variant="outline">
<a href="/registry">Connect Registry</a>
</Button>
</>
) : (
"Ask an administrator to connect Registry before adding custom providers."
)}
</AlertDescription>
</Alert>
);
}