mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
fix(ui): defer billing onboarding and add AWS button styling (#12807)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
This commit is contained in:
co-authored by
Alejandro Bailo
parent
d4b64fc29e
commit
a944a8317b
@@ -0,0 +1 @@
|
|||||||
|
AWS Marketplace button variant with outlined styling for light and dark themes
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Automatic onboarding stays hidden on billing pages and remains available after leaving billing
|
||||||
@@ -9,9 +9,11 @@ import { OnboardingGate } from "../onboarding-gate";
|
|||||||
|
|
||||||
const pushMock = vi.fn();
|
const pushMock = vi.fn();
|
||||||
const armMock = vi.fn();
|
const armMock = vi.fn();
|
||||||
|
const pathnameMock = vi.fn();
|
||||||
|
|
||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
useRouter: () => ({ push: pushMock, replace: vi.fn() }),
|
useRouter: () => ({ push: pushMock, replace: vi.fn() }),
|
||||||
|
usePathname: () => pathnameMock(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/store/onboarding-checkpoint", () => ({
|
vi.mock("@/store/onboarding-checkpoint", () => ({
|
||||||
@@ -30,12 +32,62 @@ describe("OnboardingGate", () => {
|
|||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
pushMock.mockClear();
|
pushMock.mockClear();
|
||||||
armMock.mockClear();
|
armMock.mockClear();
|
||||||
|
pathnameMock.mockReturnValue("/");
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each(["/billing", "/billing/", "/billing/checkout"])(
|
||||||
|
"defers onboarding on %s without resolving it",
|
||||||
|
(pathname) => {
|
||||||
|
// Given
|
||||||
|
pathnameMock.mockReturnValue(pathname);
|
||||||
|
|
||||||
|
// When
|
||||||
|
render(<OnboardingGate hasProviders={false} />);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(
|
||||||
|
screen.queryByRole("button", { name: /get started/i }),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(localStorageAdapter.get(addProviderTourId)).toBeNull();
|
||||||
|
expect(armMock).not.toHaveBeenCalled();
|
||||||
|
expect(pushMock).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("offers onboarding after leaving billing without remounting the gate", async () => {
|
||||||
|
// Given
|
||||||
|
pathnameMock.mockReturnValue("/billing");
|
||||||
|
const { rerender } = render(<OnboardingGate hasProviders={false} />);
|
||||||
|
|
||||||
|
// When
|
||||||
|
pathnameMock.mockReturnValue("/");
|
||||||
|
rerender(<OnboardingGate hasProviders={false} />);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(
|
||||||
|
await screen.findByRole("button", { name: /get started/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(localStorageAdapter.get(addProviderTourId)).toBeNull();
|
||||||
|
expect(armMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not suppress onboarding on a route that only shares the billing prefix", async () => {
|
||||||
|
// Given
|
||||||
|
pathnameMock.mockReturnValue("/billing-settings");
|
||||||
|
|
||||||
|
// When
|
||||||
|
render(<OnboardingGate hasProviders={false} />);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(
|
||||||
|
await screen.findByRole("button", { name: /get started/i }),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
describe("when the user has no providers and no completion record", () => {
|
describe("when the user has no providers and no completion record", () => {
|
||||||
it("shows the Welcome modal", async () => {
|
it("shows the Welcome modal", async () => {
|
||||||
render(<OnboardingGate hasProviders={false} />);
|
render(<OnboardingGate hasProviders={false} />);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { getOrderedFlows, shouldStartOnboarding } from "@/lib/onboarding";
|
import { getOrderedFlows, shouldStartOnboarding } from "@/lib/onboarding";
|
||||||
@@ -20,6 +20,10 @@ interface OnboardingGateProps {
|
|||||||
// via useSyncExternalStore — server renders nothing, no hydration mismatch.
|
// via useSyncExternalStore — server renders nothing, no hydration mismatch.
|
||||||
export function OnboardingGate({ hasProviders }: OnboardingGateProps) {
|
export function OnboardingGate({ hasProviders }: OnboardingGateProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
// Billing must stay usable before onboarding; leaving it keeps the gate eligible.
|
||||||
|
const isBillingRoute =
|
||||||
|
pathname === "/billing" || pathname?.startsWith("/billing/");
|
||||||
|
|
||||||
// Gate forces only the first flow (`add-provider`); remaining flows come via checkpoint/replay.
|
// Gate forces only the first flow (`add-provider`); remaining flows come via checkpoint/replay.
|
||||||
const flow = getOrderedFlows()[0] ?? null;
|
const flow = getOrderedFlows()[0] ?? null;
|
||||||
@@ -32,6 +36,7 @@ export function OnboardingGate({ hasProviders }: OnboardingGateProps) {
|
|||||||
|
|
||||||
const activeFlow =
|
const activeFlow =
|
||||||
flow &&
|
flow &&
|
||||||
|
!isBillingRoute &&
|
||||||
!resolvedThisSession &&
|
!resolvedThisSession &&
|
||||||
shouldStartOnboarding({ hasProviders, completionRecord })
|
shouldStartOnboarding({ hasProviders, completionRecord })
|
||||||
? flow
|
? flow
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ const buttonVariants = cva(
|
|||||||
"border border-transparent bg-bg-fail text-white hover:bg-bg-fail/90 active:bg-bg-fail/80 focus-visible:ring-bg-fail/50",
|
"border border-transparent bg-bg-fail text-white hover:bg-bg-fail/90 active:bg-bg-fail/80 focus-visible:ring-bg-fail/50",
|
||||||
outline:
|
outline:
|
||||||
"border border-border-neutral-secondary bg-bg-neutral-secondary hover:bg-bg-neutral-tertiary active:bg-border-neutral-tertiary text-text-neutral-primary focus-visible:ring-border-neutral-tertiary/50",
|
"border border-border-neutral-secondary bg-bg-neutral-secondary hover:bg-bg-neutral-tertiary active:bg-border-neutral-tertiary text-text-neutral-primary focus-visible:ring-border-neutral-tertiary/50",
|
||||||
|
"aws-marketplace":
|
||||||
|
"border-2 border-button-aws-marketplace bg-transparent text-button-aws-marketplace font-semibold hover:bg-button-aws-marketplace/5 active:bg-button-aws-marketplace/10 focus-visible:ring-button-aws-marketplace",
|
||||||
ghost:
|
ghost:
|
||||||
"border border-transparent text-text-neutral-primary hover:bg-bg-neutral-tertiary active:bg-border-neutral-secondary focus-visible:ring-border-neutral-secondary/50",
|
"border border-transparent text-text-neutral-primary hover:bg-bg-neutral-tertiary active:bg-border-neutral-secondary focus-visible:ring-border-neutral-secondary/50",
|
||||||
link: "text-button-tertiary underline-offset-4 hover:text-button-tertiary-hover disabled:bg-transparent",
|
link: "text-button-tertiary underline-offset-4 hover:text-button-tertiary-hover disabled:bg-transparent",
|
||||||
|
|||||||
@@ -92,6 +92,7 @@
|
|||||||
--bg-button-tertiary-hover: var(--color-blue-500);
|
--bg-button-tertiary-hover: var(--color-blue-500);
|
||||||
--bg-button-tertiary-active: var(--color-indigo-600);
|
--bg-button-tertiary-active: var(--color-indigo-600);
|
||||||
--bg-button-disabled: var(--color-neutral-300);
|
--bg-button-disabled: var(--color-neutral-300);
|
||||||
|
--button-aws-marketplace: #232f3e;
|
||||||
|
|
||||||
/* Radar Map */
|
/* Radar Map */
|
||||||
--bg-radar-map: #b51c8033;
|
--bg-radar-map: #b51c8033;
|
||||||
@@ -218,6 +219,7 @@
|
|||||||
--bg-button-tertiary-hover: var(--color-blue-400);
|
--bg-button-tertiary-hover: var(--color-blue-400);
|
||||||
--bg-button-tertiary-active: var(--color-blue-600);
|
--bg-button-tertiary-active: var(--color-blue-600);
|
||||||
--bg-button-disabled: var(--color-neutral-700);
|
--bg-button-disabled: var(--color-neutral-700);
|
||||||
|
--button-aws-marketplace: var(--color-white);
|
||||||
|
|
||||||
/* Neutral Map */
|
/* Neutral Map */
|
||||||
--bg-neutral-map: var(--color-gray-800);
|
--bg-neutral-map: var(--color-gray-800);
|
||||||
@@ -330,6 +332,7 @@
|
|||||||
--color-button-tertiary-hover: var(--bg-button-tertiary-hover);
|
--color-button-tertiary-hover: var(--bg-button-tertiary-hover);
|
||||||
--color-button-tertiary-active: var(--bg-button-tertiary-active);
|
--color-button-tertiary-active: var(--bg-button-tertiary-active);
|
||||||
--color-button-disabled: var(--bg-button-disabled);
|
--color-button-disabled: var(--bg-button-disabled);
|
||||||
|
--color-button-aws-marketplace: var(--button-aws-marketplace);
|
||||||
|
|
||||||
/* Input Colors */
|
/* Input Colors */
|
||||||
--color-bg-input-primary: var(--bg-input-primary);
|
--color-bg-input-primary: var(--bg-input-primary);
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user