From 7a64e1a1d15b36880082b4b852ebd2e30aefbfae Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:59:00 +0200 Subject: [PATCH] feat(ui): add cancelled subscription variant to the trial sidebar banner (#12538) --- .../trial-sidebar-banner-cancelled.added.md | 1 + .../app-sidebar/trial-sidebar-banner.test.tsx | 23 +++++++++++++ .../app-sidebar/trial-sidebar-banner.tsx | 34 +++++++++++++++---- ui/next.config.js | 8 +++++ 4 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 ui/changelog.d/trial-sidebar-banner-cancelled.added.md diff --git a/ui/changelog.d/trial-sidebar-banner-cancelled.added.md b/ui/changelog.d/trial-sidebar-banner-cancelled.added.md new file mode 100644 index 0000000000..baf343b974 --- /dev/null +++ b/ui/changelog.d/trial-sidebar-banner-cancelled.added.md @@ -0,0 +1 @@ +Cancelled-subscription variant in the sidebar trial banner (Prowler Cloud only) diff --git a/ui/components/layout/app-sidebar/trial-sidebar-banner.test.tsx b/ui/components/layout/app-sidebar/trial-sidebar-banner.test.tsx index 0df51b92ea..284add8978 100644 --- a/ui/components/layout/app-sidebar/trial-sidebar-banner.test.tsx +++ b/ui/components/layout/app-sidebar/trial-sidebar-banner.test.tsx @@ -148,6 +148,29 @@ describe("TrialSidebarBanner", () => { ).toHaveAttribute("href", "/billing"); }); + it("renders the cancelled subscription card with the terminal treatment", () => { + // Given / When + render( + , + ); + + // Then + const banner = screen.getByRole("status", { + name: "Cancelled subscription", + }); + expect(banner).toHaveTextContent("Subscription cancelled"); + expect(banner).toHaveTextContent("Subscription required"); + expect(banner).toHaveTextContent( + "Scans are paused and your assessment data remains available. Subscribe again to continue scanning.", + ); + expect(banner).toHaveAttribute("data-urgency", "critical"); + expect( + screen.getByRole("link", { + name: "Explore plans after your cancelled subscription", + }), + ).toHaveAttribute("href", "/billing"); + }); + it("invokes the sidebar selection callback from the billing CTA", async () => { // Given const user = userEvent.setup(); diff --git a/ui/components/layout/app-sidebar/trial-sidebar-banner.tsx b/ui/components/layout/app-sidebar/trial-sidebar-banner.tsx index 7a42976164..023d9c36c3 100644 --- a/ui/components/layout/app-sidebar/trial-sidebar-banner.tsx +++ b/ui/components/layout/app-sidebar/trial-sidebar-banner.tsx @@ -20,6 +20,7 @@ export const TRIAL_SIDEBAR_BANNER_VARIANT = { ACTIVE_SCANS: "active_scans", ACTIVE_UNLIMITED: "active_unlimited", EXPIRED: "expired", + CANCELLED: "cancelled", } as const; interface TrialSidebarBannerBaseProps { @@ -51,11 +52,17 @@ interface ExpiredTrialSidebarBannerProps extends TrialSidebarBannerBaseProps { remaining?: never; } +interface CancelledTrialSidebarBannerProps extends TrialSidebarBannerBaseProps { + variant: typeof TRIAL_SIDEBAR_BANNER_VARIANT.CANCELLED; + remaining?: never; +} + export type TrialSidebarBannerProps = | ActiveDaysTrialSidebarBannerProps | ActiveScansTrialSidebarBannerProps | ActiveUnlimitedTrialSidebarBannerProps - | ExpiredTrialSidebarBannerProps; + | ExpiredTrialSidebarBannerProps + | CancelledTrialSidebarBannerProps; const TRIAL_URGENCY = { HEALTHY: "healthy", @@ -115,6 +122,12 @@ const TRIAL_SIDEBAR_BANNER_COPY: Record< linkLabel: "Explore plans after your trial expired", cardLabel: "Expired trial", }, + [TRIAL_SIDEBAR_BANNER_VARIANT.CANCELLED]: { + badge: "Subscription cancelled", + body: "Scans are paused and your assessment data remains available. Subscribe again to continue scanning.", + linkLabel: "Explore plans after your cancelled subscription", + cardLabel: "Cancelled subscription", + }, }; const TRIAL_SIDEBAR_BANNER_METER: Record< @@ -150,7 +163,10 @@ const TRIAL_URGENCY_STYLES: Record = { }; const getTrialUrgency = (props: TrialSidebarBannerProps): TrialUrgency => { - if (props.variant === TRIAL_SIDEBAR_BANNER_VARIANT.EXPIRED) { + if ( + props.variant === TRIAL_SIDEBAR_BANNER_VARIANT.EXPIRED || + props.variant === TRIAL_SIDEBAR_BANNER_VARIANT.CANCELLED + ) { return TRIAL_URGENCY.CRITICAL; } if ( @@ -181,7 +197,11 @@ const TILT_RANGE_Y = 2; const TILT_LIFT = -2; export const TrialSidebarBanner = (props: TrialSidebarBannerProps) => { - const isExpired = props.variant === TRIAL_SIDEBAR_BANNER_VARIANT.EXPIRED; + // Expired trial and cancelled subscription share the terminal treatment: + // error badge, error icon, "Subscription required" heading. + const isTerminal = + props.variant === TRIAL_SIDEBAR_BANNER_VARIANT.EXPIRED || + props.variant === TRIAL_SIDEBAR_BANNER_VARIANT.CANCELLED; const isScanBased = props.variant === TRIAL_SIDEBAR_BANNER_VARIANT.ACTIVE_SCANS; // The backend keeps a scan-capped trial `active` once its quota is spent. @@ -189,7 +209,7 @@ export const TrialSidebarBanner = (props: TrialSidebarBannerProps) => { const urgency = getTrialUrgency(props); const urgencyStyles = TRIAL_URGENCY_STYLES[urgency]; const copy = TRIAL_SIDEBAR_BANNER_COPY[props.variant]; - const heading = isExpired + const heading = isTerminal ? "Subscription required" : props.variant === TRIAL_SIDEBAR_BANNER_VARIANT.ACTIVE_UNLIMITED ? "Trial active" @@ -282,14 +302,16 @@ export const TrialSidebarBanner = (props: TrialSidebarBannerProps) => {