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