diff --git a/libs/portal-plugin-onboarding/src/__tests__/components/OnboardingChecklist.test.tsx b/libs/portal-plugin-onboarding/src/__tests__/components/OnboardingChecklist.test.tsx index de7413dad..75c75694d 100644 --- a/libs/portal-plugin-onboarding/src/__tests__/components/OnboardingChecklist.test.tsx +++ b/libs/portal-plugin-onboarding/src/__tests__/components/OnboardingChecklist.test.tsx @@ -5,18 +5,21 @@ import { OnboardingIntent } from "@/types"; import { OnboardingChecklist } from "@/ui/components/OnboardingChecklist"; const mockStepsPinning = [ + { id: "docs", isComplete: true, label: "Read the Docs", description: "Learn how to pin content, manage CIDs, and automate your workflow", ctaLabel: "Browse docs", ctaRoute: null, docsUrl: "https://docs.pinner.xyz/ipfs/quickstart" }, { id: "cli", isComplete: false, label: "Install CLI", description: "Copy the command below, paste it into your terminal, and run it to install the Pinner CLI", ctaLabel: "Copy install command", ctaRoute: null }, { id: "subscribe", isComplete: false, label: "Subscribe", description: "Choose a plan to start pinning content to the IPFS network", ctaLabel: "View plans", ctaRoute: "/account/subscription" }, { id: "upload", isComplete: false, label: "Upload Content", description: "Pin your first file or directory to IPFS", ctaLabel: "Upload files", ctaRoute: "/services/ipfs/files" }, ]; const mockStepsHosting = [ + { id: "docs", isComplete: true, label: "Read the Docs", description: "Learn how to deploy websites and manage domains", ctaLabel: "Browse docs", ctaRoute: null, docsUrl: "https://docs.pinner.xyz/web/quickstart" }, { id: "cli", isComplete: false, label: "Install CLI", description: "Copy the command below, paste it into your terminal, and run it to install the Pinner CLI", ctaLabel: "Copy install command", ctaRoute: null }, { id: "subscribe", isComplete: false, label: "Subscribe", description: "Choose a plan to start hosting websites on IPFS", ctaLabel: "View plans", ctaRoute: "/account/subscription" }, { id: "deploy", isComplete: false, label: "Deploy Website", description: "Deploy your first website to IPFS", ctaLabel: "Create website", ctaRoute: "/websites" }, ]; const mockStepsComplete = [ + { id: "docs", isComplete: true, label: "Read the Docs", description: "Learn how to pin content, manage CIDs, and automate your workflow", ctaLabel: "Browse docs", ctaRoute: null, docsUrl: "https://docs.pinner.xyz/ipfs/quickstart" }, { id: "cli", isComplete: true, label: "Install CLI", description: "Copy the command below, paste it into your terminal, and run it to install the Pinner CLI", ctaLabel: "Copy install command", ctaRoute: null }, { id: "subscribe", isComplete: true, label: "Subscribe", description: "Choose a plan to start pinning content to the IPFS network", ctaLabel: "View plans", ctaRoute: "/account/subscription" }, { id: "upload", isComplete: true, label: "Upload Content", description: "Pin your first file or directory to IPFS", ctaLabel: "Upload files", ctaRoute: "/services/ipfs/files" }, @@ -145,7 +148,7 @@ describe("OnboardingChecklist", () => { await expect.element(page.getByText("Unable to load onboarding status")).toBeVisible(); }); - it("shows full checklist with 3 steps for pinning intent", async () => { + it("shows full checklist with 4 steps for pinning intent", async () => { mockUseOnboardingStatus.mockReturnValue({ steps: mockStepsPinning, completedCount: 0, @@ -158,10 +161,10 @@ describe("OnboardingChecklist", () => { await expect.element(page.getByTestId("step-cli")).toBeVisible(); await expect.element(page.getByTestId("step-subscribe")).toBeVisible(); await expect.element(page.getByTestId("step-upload")).toBeVisible(); - await expect.element(page.getByText("0 of 3")).toBeVisible(); + await expect.element(page.getByText("0 of 4")).toBeVisible(); }); - it("shows 3 steps for hosting intent", async () => { + it("shows 4 steps for hosting intent", async () => { mockUseOnboardingStatus.mockReturnValue({ steps: mockStepsHosting, completedCount: 0, @@ -174,7 +177,7 @@ describe("OnboardingChecklist", () => { await expect.element(page.getByTestId("step-cli")).toBeVisible(); await expect.element(page.getByTestId("step-subscribe")).toBeVisible(); await expect.element(page.getByTestId("step-deploy")).toBeVisible(); - await expect.element(page.getByText("0 of 3")).toBeVisible(); + await expect.element(page.getByText("0 of 4")).toBeVisible(); }); it("shows compact banner when dismissed", async () => { diff --git a/libs/portal-plugin-onboarding/src/__tests__/hooks/useOnboardingStatus.test.ts b/libs/portal-plugin-onboarding/src/__tests__/hooks/useOnboardingStatus.test.ts index 20dccf279..0f6488810 100644 --- a/libs/portal-plugin-onboarding/src/__tests__/hooks/useOnboardingStatus.test.ts +++ b/libs/portal-plugin-onboarding/src/__tests__/hooks/useOnboardingStatus.test.ts @@ -47,47 +47,51 @@ describe("useOnboardingStatus", () => { }); describe("default (null) intent — falls back to pinning", () => { - it("returns 3 pinning steps with correct IDs, labels, and CTAs", async () => { + it("returns 4 pinning steps with correct IDs, labels, and CTAs", async () => { const { result } = await renderHook(() => useOnboardingStatus()); - expect(result.current.steps).toHaveLength(3); - expect(result.current.steps[0].id).toBe("cli"); - expect(result.current.steps[0].label).toBe("Install CLI"); - expect(result.current.steps[0].ctaLabel).toBe("Copy install command"); + expect(result.current.steps).toHaveLength(4); + expect(result.current.steps[0].id).toBe("docs"); + expect(result.current.steps[0].label).toBe("Read the Docs"); + expect(result.current.steps[0].ctaLabel).toBe("Browse docs"); expect(result.current.steps[0].ctaRoute).toBeNull(); - expect(result.current.steps[1].id).toBe("subscribe"); - expect(result.current.steps[1].label).toBe("Subscribe"); - expect(result.current.steps[1].ctaLabel).toBe("View plans"); - expect(result.current.steps[1].ctaRoute).toBe("/account/subscription"); - expect(result.current.steps[2].id).toBe("upload"); - expect(result.current.steps[2].label).toBe("Upload Content"); - expect(result.current.steps[2].ctaLabel).toBe("Upload files"); - expect(result.current.steps[2].ctaRoute).toBe("/services/ipfs/files"); + expect(result.current.steps[1].id).toBe("cli"); + expect(result.current.steps[1].label).toBe("Install CLI"); + expect(result.current.steps[1].ctaLabel).toBe("Copy install command"); + expect(result.current.steps[1].ctaRoute).toBeNull(); + expect(result.current.steps[2].id).toBe("subscribe"); + expect(result.current.steps[2].label).toBe("Subscribe"); + expect(result.current.steps[2].ctaLabel).toBe("View plans"); + expect(result.current.steps[2].ctaRoute).toBe("/account/subscription"); + expect(result.current.steps[3].id).toBe("upload"); + expect(result.current.steps[3].label).toBe("Upload Content"); + expect(result.current.steps[3].ctaLabel).toBe("Upload files"); + expect(result.current.steps[3].ctaRoute).toBe("/services/ipfs/files"); }); it("uses hasPins for upload step completion when intent is null", async () => { mockUseHasPins.mockReturnValue({ hasPins: true, isBusy: false, hasError: false }); const { result } = await renderHook(() => useOnboardingStatus()); - expect(result.current.steps[2].isComplete).toBe(true); + expect(result.current.steps[3].isComplete).toBe(true); }); it("completedCount is correct for partial completion", async () => { mockUseCliInstalled.mockReturnValue({ isInstalled: true, isBusy: false, hasError: false }); const { result } = await renderHook(() => useOnboardingStatus()); - expect(result.current.completedCount).toBe(1); + expect(result.current.completedCount).toBe(2); }); - it("completedCount is 2 when two steps complete", async () => { + it("completedCount is 3 when two tracked steps complete", async () => { mockUseCliInstalled.mockReturnValue({ isInstalled: true, isBusy: false, hasError: false }); mockUseIsSubscribed.mockReturnValue({ isSubscribed: true, isBusy: false, hasError: false }); const { result } = await renderHook(() => useOnboardingStatus()); - expect(result.current.completedCount).toBe(2); + expect(result.current.completedCount).toBe(3); }); - it("isComplete is true only when all 3 steps complete", async () => { + it("isComplete is true only when all tracked steps complete", async () => { mockUseCliInstalled.mockReturnValue({ isInstalled: true, isBusy: false, hasError: false }); mockUseIsSubscribed.mockReturnValue({ isSubscribed: true, isBusy: false, hasError: false }); @@ -98,7 +102,7 @@ describe("useOnboardingStatus", () => { const { result: result2 } = await renderHook(() => useOnboardingStatus()); expect(result2.current.isComplete).toBe(true); - expect(result2.current.completedCount).toBe(3); + expect(result2.current.completedCount).toBe(4); }); it("isBusy is true when any child hook is busy", async () => { @@ -124,29 +128,33 @@ describe("useOnboardingStatus", () => { mockReadPersistedParam.mockResolvedValue(OnboardingIntent.Hosting); }); - it("returns 3 hosting steps with correct IDs, labels, and CTAs", async () => { + it("returns 4 hosting steps with correct IDs, labels, and CTAs", async () => { const { result } = await renderHook(() => useOnboardingStatus()); - expect(result.current.steps).toHaveLength(3); - expect(result.current.steps[0].id).toBe("cli"); - expect(result.current.steps[0].label).toBe("Install CLI"); - expect(result.current.steps[0].ctaLabel).toBe("Copy install command"); + expect(result.current.steps).toHaveLength(4); + expect(result.current.steps[0].id).toBe("docs"); + expect(result.current.steps[0].label).toBe("Read the Docs"); + expect(result.current.steps[0].ctaLabel).toBe("Browse docs"); expect(result.current.steps[0].ctaRoute).toBeNull(); - expect(result.current.steps[1].id).toBe("subscribe"); - expect(result.current.steps[1].label).toBe("Subscribe"); - expect(result.current.steps[1].ctaLabel).toBe("View plans"); - expect(result.current.steps[1].ctaRoute).toBe("/account/subscription"); - expect(result.current.steps[2].id).toBe("deploy"); - expect(result.current.steps[2].label).toBe("Deploy Website"); - expect(result.current.steps[2].ctaLabel).toBe("Create website"); - expect(result.current.steps[2].ctaRoute).toBe("/websites"); + expect(result.current.steps[1].id).toBe("cli"); + expect(result.current.steps[1].label).toBe("Install CLI"); + expect(result.current.steps[1].ctaLabel).toBe("Copy install command"); + expect(result.current.steps[1].ctaRoute).toBeNull(); + expect(result.current.steps[2].id).toBe("subscribe"); + expect(result.current.steps[2].label).toBe("Subscribe"); + expect(result.current.steps[2].ctaLabel).toBe("View plans"); + expect(result.current.steps[2].ctaRoute).toBe("/account/subscription"); + expect(result.current.steps[3].id).toBe("deploy"); + expect(result.current.steps[3].label).toBe("Deploy Website"); + expect(result.current.steps[3].ctaLabel).toBe("Create website"); + expect(result.current.steps[3].ctaRoute).toBe("/websites"); }); it("uses hasWebsites for deploy step completion", async () => { mockUseHasWebsites.mockReturnValue({ hasWebsites: true, isBusy: false, hasError: false }); const { result } = await renderHook(() => useOnboardingStatus()); - expect(result.current.steps[2].isComplete).toBe(true); + expect(result.current.steps[3].isComplete).toBe(true); }); it("ignores hasPins for deploy step completion when hosting", async () => { @@ -154,17 +162,17 @@ describe("useOnboardingStatus", () => { mockUseHasWebsites.mockReturnValue({ hasWebsites: false, isBusy: false, hasError: false }); const { result } = await renderHook(() => useOnboardingStatus()); - expect(result.current.steps[2].isComplete).toBe(false); + expect(result.current.steps[3].isComplete).toBe(false); }); - it("isComplete is true when all 3 hosting steps complete", async () => { + it("isComplete is true when all hosting steps complete", async () => { mockUseCliInstalled.mockReturnValue({ isInstalled: true, isBusy: false, hasError: false }); mockUseIsSubscribed.mockReturnValue({ isSubscribed: true, isBusy: false, hasError: false }); mockUseHasWebsites.mockReturnValue({ hasWebsites: true, isBusy: false, hasError: false }); const { result } = await renderHook(() => useOnboardingStatus()); expect(result.current.isComplete).toBe(true); - expect(result.current.completedCount).toBe(3); + expect(result.current.completedCount).toBe(4); }); it("returns hosting intent", async () => { @@ -178,21 +186,21 @@ describe("useOnboardingStatus", () => { mockReadPersistedParam.mockResolvedValue(OnboardingIntent.Pinning); }); - it("returns 3 pinning steps with upload step checking pins", async () => { + it("returns 4 pinning steps with upload step checking pins", async () => { const { result } = await renderHook(() => useOnboardingStatus()); - expect(result.current.steps).toHaveLength(3); - expect(result.current.steps[2].id).toBe("upload"); - expect(result.current.steps[2].label).toBe("Upload Content"); - expect(result.current.steps[2].ctaLabel).toBe("Upload files"); - expect(result.current.steps[2].ctaRoute).toBe("/services/ipfs/files"); + expect(result.current.steps).toHaveLength(4); + expect(result.current.steps[3].id).toBe("upload"); + expect(result.current.steps[3].label).toBe("Upload Content"); + expect(result.current.steps[3].ctaLabel).toBe("Upload files"); + expect(result.current.steps[3].ctaRoute).toBe("/services/ipfs/files"); }); it("uses hasPins for upload step completion", async () => { mockUseHasPins.mockReturnValue({ hasPins: true, isBusy: false, hasError: false }); const { result } = await renderHook(() => useOnboardingStatus()); - expect(result.current.steps[2].isComplete).toBe(true); + expect(result.current.steps[3].isComplete).toBe(true); }); it("returns pinning intent", async () => { diff --git a/libs/portal-plugin-onboarding/src/__tests__/intents/intentConfigs.test.ts b/libs/portal-plugin-onboarding/src/__tests__/intents/intentConfigs.test.ts index c3b14ff13..e3fd71428 100644 --- a/libs/portal-plugin-onboarding/src/__tests__/intents/intentConfigs.test.ts +++ b/libs/portal-plugin-onboarding/src/__tests__/intents/intentConfigs.test.ts @@ -5,48 +5,62 @@ import { INTENT_STEP_CONFIGS, DEFAULT_INTENT } from "@/intents/index"; import { OnboardingIntent } from "@/types"; describe("PINNING_STEPS", () => { - it("has 3 steps: cli, subscribe, upload", () => { - expect(PINNING_STEPS).toHaveLength(3); - expect(PINNING_STEPS[0].id).toBe("cli"); - expect(PINNING_STEPS[1].id).toBe("subscribe"); - expect(PINNING_STEPS[2].id).toBe("upload"); + it("has 4 steps: docs, cli, subscribe, upload", () => { + expect(PINNING_STEPS).toHaveLength(4); + expect(PINNING_STEPS[0].id).toBe("docs"); + expect(PINNING_STEPS[1].id).toBe("cli"); + expect(PINNING_STEPS[2].id).toBe("subscribe"); + expect(PINNING_STEPS[3].id).toBe("upload"); }); - it("cli step has null ctaRoute (clipboard copy)", () => { + it("docs step has docsUrl and null ctaRoute", () => { expect(PINNING_STEPS[0].ctaRoute).toBeNull(); - expect(PINNING_STEPS[0].ctaLabel).toBe("Copy install command"); + expect(PINNING_STEPS[0].docsUrl).toBeDefined(); + expect(PINNING_STEPS[0].ctaLabel).toBe("Browse docs"); + }); + + it("cli step has null ctaRoute (clipboard copy)", () => { + expect(PINNING_STEPS[1].ctaRoute).toBeNull(); + expect(PINNING_STEPS[1].ctaLabel).toBe("Copy install command"); }); it("subscribe step navigates to /account/subscription", () => { - expect(PINNING_STEPS[1].ctaRoute).toBe("/account/subscription"); + expect(PINNING_STEPS[2].ctaRoute).toBe("/account/subscription"); }); it("upload step navigates to /files", () => { - expect(PINNING_STEPS[2].ctaRoute).toBe("/services/ipfs/files"); - expect(PINNING_STEPS[2].ctaLabel).toBe("Upload files"); + expect(PINNING_STEPS[3].ctaRoute).toBe("/services/ipfs/files"); + expect(PINNING_STEPS[3].ctaLabel).toBe("Upload files"); }); }); describe("HOSTING_STEPS", () => { - it("has 3 steps: cli, subscribe, deploy", () => { - expect(HOSTING_STEPS).toHaveLength(3); - expect(HOSTING_STEPS[0].id).toBe("cli"); - expect(HOSTING_STEPS[1].id).toBe("subscribe"); - expect(HOSTING_STEPS[2].id).toBe("deploy"); + it("has 4 steps: docs, cli, subscribe, deploy", () => { + expect(HOSTING_STEPS).toHaveLength(4); + expect(HOSTING_STEPS[0].id).toBe("docs"); + expect(HOSTING_STEPS[1].id).toBe("cli"); + expect(HOSTING_STEPS[2].id).toBe("subscribe"); + expect(HOSTING_STEPS[3].id).toBe("deploy"); }); - it("cli step has null ctaRoute (clipboard copy)", () => { + it("docs step has docsUrl and null ctaRoute", () => { expect(HOSTING_STEPS[0].ctaRoute).toBeNull(); - expect(HOSTING_STEPS[0].ctaLabel).toBe("Copy install command"); + expect(HOSTING_STEPS[0].docsUrl).toBeDefined(); + expect(HOSTING_STEPS[0].ctaLabel).toBe("Browse docs"); + }); + + it("cli step has null ctaRoute (clipboard copy)", () => { + expect(HOSTING_STEPS[1].ctaRoute).toBeNull(); + expect(HOSTING_STEPS[1].ctaLabel).toBe("Copy install command"); }); it("subscribe step navigates to /account/subscription", () => { - expect(HOSTING_STEPS[1].ctaRoute).toBe("/account/subscription"); + expect(HOSTING_STEPS[2].ctaRoute).toBe("/account/subscription"); }); it("deploy step navigates to /websites", () => { - expect(HOSTING_STEPS[2].ctaRoute).toBe("/websites"); - expect(HOSTING_STEPS[2].ctaLabel).toBe("Create website"); + expect(HOSTING_STEPS[3].ctaRoute).toBe("/websites"); + expect(HOSTING_STEPS[3].ctaLabel).toBe("Create website"); }); }); diff --git a/libs/portal-plugin-onboarding/src/constants.ts b/libs/portal-plugin-onboarding/src/constants.ts index ad98306ca..4870b2c43 100644 --- a/libs/portal-plugin-onboarding/src/constants.ts +++ b/libs/portal-plugin-onboarding/src/constants.ts @@ -1 +1,3 @@ export const DOCS_URL = "https://docs.pinner.xyz"; +export const DOCS_PINNING_URL = "https://docs.pinner.xyz/ipfs/quickstart"; +export const DOCS_HOSTING_URL = "https://docs.pinner.xyz/web/quickstart"; diff --git a/libs/portal-plugin-onboarding/src/intents/hosting.ts b/libs/portal-plugin-onboarding/src/intents/hosting.ts index 820369f3b..1b5a7d236 100644 --- a/libs/portal-plugin-onboarding/src/intents/hosting.ts +++ b/libs/portal-plugin-onboarding/src/intents/hosting.ts @@ -1,11 +1,12 @@ import type { IntentStepConfig, OnboardingStep } from "../types"; -import { DOCS_URL } from "../constants"; +import { DOCS_HOSTING_URL } from "../constants"; import { useCliInstalled } from "../hooks/useCliInstalled"; import { useIsSubscribed } from "../hooks/useIsSubscribed"; import { useHasWebsites } from "../hooks/useHasWebsites"; -import { CLI_STEP } from "./pinning"; +import { CLI_STEP, DOCS_STEP } from "./pinning"; export const HOSTING_STEPS: IntentStepConfig[] = [ + { ...DOCS_STEP, docsUrl: DOCS_HOSTING_URL }, CLI_STEP, { id: "subscribe", @@ -20,7 +21,6 @@ export const HOSTING_STEPS: IntentStepConfig[] = [ description: "Deploy your first website to IPFS", ctaLabel: "Create website", ctaRoute: "/websites", - docsUrl: DOCS_URL, }, ]; @@ -33,9 +33,10 @@ export function useHostingSteps(active = true): { const { hasWebsites, isBusy: websitesBusy } = useHasWebsites(active); const steps: OnboardingStep[] = [ - { ...HOSTING_STEPS[0], isComplete: isInstalled }, - { ...HOSTING_STEPS[1], isComplete: isSubscribed }, - { ...HOSTING_STEPS[2], isComplete: hasWebsites }, + { ...HOSTING_STEPS[0], isComplete: true }, + { ...HOSTING_STEPS[1], isComplete: isInstalled }, + { ...HOSTING_STEPS[2], isComplete: isSubscribed }, + { ...HOSTING_STEPS[3], isComplete: hasWebsites }, ]; return { diff --git a/libs/portal-plugin-onboarding/src/intents/index.ts b/libs/portal-plugin-onboarding/src/intents/index.ts index 2293af75a..2b3e8a5ab 100644 --- a/libs/portal-plugin-onboarding/src/intents/index.ts +++ b/libs/portal-plugin-onboarding/src/intents/index.ts @@ -3,7 +3,7 @@ import type { IntentStepConfig } from "../types"; import { CLI_STEP, PINNING_STEPS, usePinningSteps } from "./pinning"; import { HOSTING_STEPS, useHostingSteps } from "./hosting"; -export { CLI_STEP } from "./pinning"; +export { CLI_STEP, DOCS_STEP } from "./pinning"; export { PINNING_STEPS } from "./pinning"; export { HOSTING_STEPS } from "./hosting"; export { usePinningSteps } from "./pinning"; diff --git a/libs/portal-plugin-onboarding/src/intents/pinning.ts b/libs/portal-plugin-onboarding/src/intents/pinning.ts index dc025226c..58486586f 100644 --- a/libs/portal-plugin-onboarding/src/intents/pinning.ts +++ b/libs/portal-plugin-onboarding/src/intents/pinning.ts @@ -1,5 +1,5 @@ import type { IntentStepConfig, OnboardingStep } from "../types"; -import { DOCS_URL } from "../constants"; +import { DOCS_PINNING_URL } from "../constants"; import { useCliInstalled } from "../hooks/useCliInstalled"; import { useIsSubscribed } from "../hooks/useIsSubscribed"; import { useHasPins } from "../hooks/useHasPins"; @@ -12,7 +12,17 @@ export const CLI_STEP: IntentStepConfig = { ctaRoute: null, }; +export const DOCS_STEP: IntentStepConfig = { + id: "docs", + label: "Read the Docs", + description: "Learn how to pin content, manage CIDs, and automate your workflow", + ctaLabel: "Browse docs", + ctaRoute: null, + docsUrl: DOCS_PINNING_URL, +}; + export const PINNING_STEPS: IntentStepConfig[] = [ + DOCS_STEP, CLI_STEP, { id: "subscribe", @@ -27,7 +37,6 @@ export const PINNING_STEPS: IntentStepConfig[] = [ description: "Pin your first file or directory to IPFS", ctaLabel: "Upload files", ctaRoute: "/services/ipfs/files", - docsUrl: DOCS_URL, }, ]; @@ -40,9 +49,10 @@ export function usePinningSteps(active = true): { const { hasPins, isBusy: pinsBusy } = useHasPins(active); const steps: OnboardingStep[] = [ - { ...PINNING_STEPS[0], isComplete: isInstalled }, - { ...PINNING_STEPS[1], isComplete: isSubscribed }, - { ...PINNING_STEPS[2], isComplete: hasPins }, + { ...PINNING_STEPS[0], isComplete: true }, + { ...PINNING_STEPS[1], isComplete: isInstalled }, + { ...PINNING_STEPS[2], isComplete: isSubscribed }, + { ...PINNING_STEPS[3], isComplete: hasPins }, ]; return { diff --git a/libs/portal-plugin-onboarding/src/ui/components/OnboardingStepCard.tsx b/libs/portal-plugin-onboarding/src/ui/components/OnboardingStepCard.tsx index 8e66057d3..b28592cc1 100644 --- a/libs/portal-plugin-onboarding/src/ui/components/OnboardingStepCard.tsx +++ b/libs/portal-plugin-onboarding/src/ui/components/OnboardingStepCard.tsx @@ -29,6 +29,16 @@ export function OnboardingStepCard({
{step.label} {step.description} + {step.docsUrl && step.ctaRoute === null && ( + + {ctaLabel} + + )}
); @@ -48,12 +58,18 @@ export function OnboardingStepCard({
{step.id === "cli" ? ( + ) : step.ctaRoute === null && step.docsUrl ? ( + + + ) : ( )} - {step.docsUrl && ( + {step.docsUrl && step.ctaRoute !== null && (