chore: unify trial expired banner

This commit is contained in:
Pepe Fagoaga
2026-06-29 11:13:46 +02:00
parent 1ddf8461fb
commit 478dc5534f
8 changed files with 80 additions and 44 deletions
@@ -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 ? (
<p className="text-text-error-primary text-sm">
You have exceeded the usage limit of one provider. You can add
more providers and run unlimited scans by adding a subscription.{" "}
<Link
href="https://cloud.prowler.com/billing"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Manage Billing
</Link>
</p>
<UsageLimitMessage />
) : isAdvanced ? (
<ScanScheduleFields
form={form}
@@ -22,6 +22,7 @@ import {
CloudFeatureBadge,
CloudFeatureBadgeLink,
} from "@/components/shared/cloud-feature-badge";
import { UsageLimitMessage } from "@/components/shared/usage-limit-message";
import { ToastAction, useToast } from "@/components/ui";
import { EntityInfo } from "@/components/ui/entities";
import {
@@ -350,20 +351,7 @@ export function LaunchStep({
</p>
)}
{(isLimitBlocked || isBlocked) && (
<p className="text-text-error-primary text-sm">
You have exceeded the usage limit of one provider. You can add more
providers and run unlimited scans by adding a subscription.{" "}
<Link
href="https://cloud.prowler.com/billing"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Manage Billing
</Link>
</p>
)}
{(isLimitBlocked || isBlocked) && <UsageLimitMessage />}
{isScheduleMode && (
<ScanScheduleFields
+3 -15
View File
@@ -18,6 +18,7 @@ import {
RadioGroupItem,
} from "@/components/shadcn/radio-group/radio-group";
import { CloudFeatureBadgeLink } from "@/components/shared/cloud-feature-badge";
import { UsageLimitMessage } from "@/components/shared/usage-limit-message";
import { FormButtons } from "@/components/ui/form";
import { toast, ToastAction } from "@/components/ui/toast";
import { getActionErrorMessage, hasActionError } from "@/lib/action-errors";
@@ -328,21 +329,6 @@ function LaunchScanForm({
</Field>
)}
{isBlocked && (
<p className="text-text-error-primary text-sm">
You have exceeded the usage limit of one provider. You can add more
providers and run unlimited scans by adding a subscription.{" "}
<Link
href="https://cloud.prowler.com/billing"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Manage Billing
</Link>
</p>
)}
{!isScheduleMode && (
<Field>
<FieldLabel htmlFor="launch-scan-alias">Alias (optional)</FieldLabel>
@@ -355,6 +341,8 @@ function LaunchScanForm({
</Field>
)}
{isBlocked && <UsageLimitMessage />}
{isScheduleMode && isScheduleLoading && (
<div className="flex items-center gap-3 py-2">
<Loader2 className="size-5 animate-spin" />
@@ -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(<UsageLimitMessage />);
expect(screen.getByText(/exceeded the usage limit/i)).toBeInTheDocument();
});
it("links to Prowler Cloud billing", () => {
render(<UsageLimitMessage />);
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(<UsageLimitMessage />);
expect(screen.getByText(USAGE_LIMIT_MESSAGE)).toBeInTheDocument();
});
it("merges a custom className with the base styles", () => {
const { container } = render(<UsageLimitMessage className="mt-4" />);
expect(container.firstChild).toHaveClass("mt-4");
expect(container.firstChild).toHaveClass("text-text-error-primary");
});
});
@@ -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) => (
<p className={cn("text-text-error-primary text-sm", className)}>
{USAGE_LIMIT_MESSAGE}{" "}
<Link
href={BILLING_URL}
target="_blank"
rel="noopener noreferrer"
className="underline"
>
Manage Billing
</Link>
</p>
);
+1 -1
View File
@@ -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.",
+7 -2
View File
@@ -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<ActionErrorStatus, string>;
+3
View File
@@ -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.