From 478dc5534ff54fb6857f11d0749664d5aa44866b Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 29 Jun 2026 11:13:46 +0200 Subject: [PATCH] chore: unify trial expired banner --- .../organizations/org-launch-scan.tsx | 14 +------ .../providers/wizard/steps/launch-step.tsx | 16 +------- ui/components/scans/launch-scan-modal.tsx | 18 ++------- .../shared/usage-limit-message.test.tsx | 37 +++++++++++++++++++ ui/components/shared/usage-limit-message.tsx | 25 +++++++++++++ ui/lib/action-errors.test.ts | 2 +- ui/lib/action-errors.ts | 9 ++++- ui/lib/external-urls.ts | 3 ++ 8 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 ui/components/shared/usage-limit-message.test.tsx create mode 100644 ui/components/shared/usage-limit-message.tsx diff --git a/ui/components/providers/organizations/org-launch-scan.tsx b/ui/components/providers/organizations/org-launch-scan.tsx index 8747d28126..90711033c6 100644 --- a/ui/components/providers/organizations/org-launch-scan.tsx +++ b/ui/components/providers/organizations/org-launch-scan.tsx @@ -23,6 +23,7 @@ import { } from "@/components/shadcn/select/select"; import { Spinner } from "@/components/shadcn/spinner/spinner"; import { TreeStatusIcon } from "@/components/shadcn/tree-view/tree-status-icon"; +import { UsageLimitMessage } from "@/components/shared/usage-limit-message"; import { ToastAction, useToast } from "@/components/ui"; import { getActionErrorMessage, hasActionError } from "@/lib/action-errors"; import { @@ -362,18 +363,7 @@ export function OrgLaunchScan({ )} {isBlocked ? ( -

- You have exceeded the usage limit of one provider. You can add - more providers and run unlimited scans by adding a subscription.{" "} - - Manage Billing - -

+ ) : isAdvanced ? ( )} - {(isLimitBlocked || isBlocked) && ( -

- You have exceeded the usage limit of one provider. You can add more - providers and run unlimited scans by adding a subscription.{" "} - - Manage Billing - -

- )} + {(isLimitBlocked || isBlocked) && } {isScheduleMode && ( )} - {isBlocked && ( -

- You have exceeded the usage limit of one provider. You can add more - providers and run unlimited scans by adding a subscription.{" "} - - Manage Billing - -

- )} - {!isScheduleMode && ( Alias (optional) @@ -355,6 +341,8 @@ function LaunchScanForm({ )} + {isBlocked && } + {isScheduleMode && isScheduleLoading && (
diff --git a/ui/components/shared/usage-limit-message.test.tsx b/ui/components/shared/usage-limit-message.test.tsx new file mode 100644 index 0000000000..b7fdf04954 --- /dev/null +++ b/ui/components/shared/usage-limit-message.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import { USAGE_LIMIT_MESSAGE } from "@/lib/action-errors"; +import { BILLING_URL } from "@/lib/external-urls"; + +import { UsageLimitMessage } from "./usage-limit-message"; + +describe("UsageLimitMessage", () => { + it("renders the shared usage-limit copy", () => { + render(); + + expect(screen.getByText(/exceeded the usage limit/i)).toBeInTheDocument(); + }); + + it("links to Prowler Cloud billing", () => { + render(); + + const link = screen.getByRole("link", { name: /manage billing/i }); + expect(link).toHaveAttribute("href", BILLING_URL); + expect(link).toHaveAttribute("target", "_blank"); + expect(link).toHaveAttribute("rel", "noopener noreferrer"); + }); + + it("keeps the copy in sync with the 402 action-error message", () => { + render(); + + expect(screen.getByText(USAGE_LIMIT_MESSAGE)).toBeInTheDocument(); + }); + + it("merges a custom className with the base styles", () => { + const { container } = render(); + + expect(container.firstChild).toHaveClass("mt-4"); + expect(container.firstChild).toHaveClass("text-text-error-primary"); + }); +}); diff --git a/ui/components/shared/usage-limit-message.tsx b/ui/components/shared/usage-limit-message.tsx new file mode 100644 index 0000000000..8a413e4f61 --- /dev/null +++ b/ui/components/shared/usage-limit-message.tsx @@ -0,0 +1,25 @@ +import Link from "next/link"; + +import { USAGE_LIMIT_MESSAGE } from "@/lib/action-errors"; +import { BILLING_URL } from "@/lib/external-urls"; +import { cn } from "@/lib/utils"; + +interface UsageLimitMessageProps { + className?: string; +} + +// Over-limit (trial-expired) notice shown in scan launch flows. Pairs the shared +// usage-limit copy with a link to Prowler Cloud billing. +export const UsageLimitMessage = ({ className }: UsageLimitMessageProps) => ( +

+ {USAGE_LIMIT_MESSAGE}{" "} + + Manage Billing + +

+); diff --git a/ui/lib/action-errors.test.ts b/ui/lib/action-errors.test.ts index b770f6b7af..5c434dab5b 100644 --- a/ui/lib/action-errors.test.ts +++ b/ui/lib/action-errors.test.ts @@ -22,7 +22,7 @@ describe("getActionErrorMessage", () => { expect(message).toBe(ACTION_ERROR_MESSAGES[ACTION_ERROR_STATUS.FORBIDDEN]); }); - it("should use the default subscription error for payment-required responses", () => { + it("should use the default usage-limit error for payment-required responses", () => { // Given const result = { error: "Payment required.", diff --git a/ui/lib/action-errors.ts b/ui/lib/action-errors.ts index 46fc3ae4b8..bea6dcd8df 100644 --- a/ui/lib/action-errors.ts +++ b/ui/lib/action-errors.ts @@ -6,9 +6,14 @@ export const ACTION_ERROR_STATUS = { export type ActionErrorStatus = (typeof ACTION_ERROR_STATUS)[keyof typeof ACTION_ERROR_STATUS]; +// Shown whenever the API returns 402 for an over-limit (trial-expired) tenant. +// Rendered with a billing link by he `UsageLimitMessage` component, and as +// plain text in toasts/field errors. +export const USAGE_LIMIT_MESSAGE = + "You have exceeded the usage limit of one provider. You can add more providers and run unlimited scans by adding a subscription."; + export const ACTION_ERROR_MESSAGES = { - [ACTION_ERROR_STATUS.PAYMENT_REQUIRED]: - "Your subscription doesn't allow this action. Upgrade your plan or contact an administrator.", + [ACTION_ERROR_STATUS.PAYMENT_REQUIRED]: USAGE_LIMIT_MESSAGE, [ACTION_ERROR_STATUS.FORBIDDEN]: "You don't have permission to perform this action. Ask an administrator to update your role.", } as const satisfies Record; diff --git a/ui/lib/external-urls.ts b/ui/lib/external-urls.ts index f56461e7a5..c818fea8d3 100644 --- a/ui/lib/external-urls.ts +++ b/ui/lib/external-urls.ts @@ -19,6 +19,9 @@ export const DOCS_URLS = { export const PROWLER_CF_TEMPLATE_URL = "https://prowler-cloud-public.s3.eu-west-1.amazonaws.com/permissions/templates/aws/cloudformation/prowler-scan-role.yml"; +// Prowler Cloud billing/subscription management page. +export const BILLING_URL = "https://cloud.prowler.com/billing"; + // AWS Console URL for creating a new StackSet. // Hardcoded to us-east-1 — StackSets are typically managed from this region. // Users in AWS GovCloud or China partitions would need different URLs.