diff --git a/ui/changelog.d/aws-marketplace-button.added.md b/ui/changelog.d/aws-marketplace-button.added.md new file mode 100644 index 0000000000..235112967f --- /dev/null +++ b/ui/changelog.d/aws-marketplace-button.added.md @@ -0,0 +1 @@ +AWS Marketplace button variant with outlined styling for light and dark themes diff --git a/ui/changelog.d/defer-onboarding-on-billing.fixed.md b/ui/changelog.d/defer-onboarding-on-billing.fixed.md new file mode 100644 index 0000000000..5afcc36bcc --- /dev/null +++ b/ui/changelog.d/defer-onboarding-on-billing.fixed.md @@ -0,0 +1 @@ +Automatic onboarding stays hidden on billing pages and remains available after leaving billing diff --git a/ui/components/onboarding/__tests__/onboarding-gate.test.tsx b/ui/components/onboarding/__tests__/onboarding-gate.test.tsx index 4198ba5846..9a5f7cf88e 100644 --- a/ui/components/onboarding/__tests__/onboarding-gate.test.tsx +++ b/ui/components/onboarding/__tests__/onboarding-gate.test.tsx @@ -9,9 +9,11 @@ import { OnboardingGate } from "../onboarding-gate"; const pushMock = vi.fn(); const armMock = vi.fn(); +const pathnameMock = vi.fn(); vi.mock("next/navigation", () => ({ useRouter: () => ({ push: pushMock, replace: vi.fn() }), + usePathname: () => pathnameMock(), })); vi.mock("@/store/onboarding-checkpoint", () => ({ @@ -30,12 +32,62 @@ describe("OnboardingGate", () => { window.localStorage.clear(); pushMock.mockClear(); armMock.mockClear(); + pathnameMock.mockReturnValue("/"); }); afterEach(() => { vi.restoreAllMocks(); }); + it.each(["/billing", "/billing/", "/billing/checkout"])( + "defers onboarding on %s without resolving it", + (pathname) => { + // Given + pathnameMock.mockReturnValue(pathname); + + // When + render(); + + // 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(); + + // When + pathnameMock.mockReturnValue("/"); + rerender(); + + // 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(); + + // Then + expect( + await screen.findByRole("button", { name: /get started/i }), + ).toBeInTheDocument(); + }); + describe("when the user has no providers and no completion record", () => { it("shows the Welcome modal", async () => { render(); diff --git a/ui/components/onboarding/onboarding-gate.tsx b/ui/components/onboarding/onboarding-gate.tsx index 0161bffef4..f32d411938 100644 --- a/ui/components/onboarding/onboarding-gate.tsx +++ b/ui/components/onboarding/onboarding-gate.tsx @@ -1,6 +1,6 @@ "use client"; -import { useRouter } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useState } from "react"; import { getOrderedFlows, shouldStartOnboarding } from "@/lib/onboarding"; @@ -20,6 +20,10 @@ interface OnboardingGateProps { // via useSyncExternalStore — server renders nothing, no hydration mismatch. export function OnboardingGate({ hasProviders }: OnboardingGateProps) { 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. const flow = getOrderedFlows()[0] ?? null; @@ -32,6 +36,7 @@ export function OnboardingGate({ hasProviders }: OnboardingGateProps) { const activeFlow = flow && + !isBillingRoute && !resolvedThisSession && shouldStartOnboarding({ hasProviders, completionRecord }) ? flow diff --git a/ui/components/shadcn/button/button.tsx b/ui/components/shadcn/button/button.tsx index f34347507b..bc37c08bc9 100644 --- a/ui/components/shadcn/button/button.tsx +++ b/ui/components/shadcn/button/button.tsx @@ -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", 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", + "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: "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", diff --git a/ui/styles/globals.css b/ui/styles/globals.css index d879b452d2..c8f60e2992 100644 --- a/ui/styles/globals.css +++ b/ui/styles/globals.css @@ -92,6 +92,7 @@ --bg-button-tertiary-hover: var(--color-blue-500); --bg-button-tertiary-active: var(--color-indigo-600); --bg-button-disabled: var(--color-neutral-300); + --button-aws-marketplace: #232f3e; /* Radar Map */ --bg-radar-map: #b51c8033; @@ -218,6 +219,7 @@ --bg-button-tertiary-hover: var(--color-blue-400); --bg-button-tertiary-active: var(--color-blue-600); --bg-button-disabled: var(--color-neutral-700); + --button-aws-marketplace: var(--color-white); /* Neutral Map */ --bg-neutral-map: var(--color-gray-800); @@ -330,6 +332,7 @@ --color-button-tertiary-hover: var(--bg-button-tertiary-hover); --color-button-tertiary-active: var(--bg-button-tertiary-active); --color-button-disabled: var(--bg-button-disabled); + --color-button-aws-marketplace: var(--button-aws-marketplace); /* Input Colors */ --color-bg-input-primary: var(--bg-input-primary); diff --git a/ui/tests/onboarding/evidence/aws-marketplace-button-desktop.png b/ui/tests/onboarding/evidence/aws-marketplace-button-desktop.png new file mode 100644 index 0000000000..ddfac1e755 Binary files /dev/null and b/ui/tests/onboarding/evidence/aws-marketplace-button-desktop.png differ diff --git a/ui/tests/onboarding/evidence/aws-marketplace-button-mobile.png b/ui/tests/onboarding/evidence/aws-marketplace-button-mobile.png new file mode 100644 index 0000000000..b0009e3b42 Binary files /dev/null and b/ui/tests/onboarding/evidence/aws-marketplace-button-mobile.png differ diff --git a/ui/tests/onboarding/evidence/aws-marketplace-button-tablet.png b/ui/tests/onboarding/evidence/aws-marketplace-button-tablet.png new file mode 100644 index 0000000000..30a2839c77 Binary files /dev/null and b/ui/tests/onboarding/evidence/aws-marketplace-button-tablet.png differ