Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 });

Expand All @@ -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 () => {
Expand All @@ -124,47 +128,51 @@ 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 () => {
mockUseHasPins.mockReturnValue({ hasPins: true, isBusy: false, hasError: false });
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 () => {
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});

Expand Down
2 changes: 2 additions & 0 deletions libs/portal-plugin-onboarding/src/constants.ts
Original file line number Diff line number Diff line change
@@ -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";
13 changes: 7 additions & 6 deletions libs/portal-plugin-onboarding/src/intents/hosting.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -20,7 +21,6 @@ export const HOSTING_STEPS: IntentStepConfig[] = [
description: "Deploy your first website to IPFS",
ctaLabel: "Create website",
ctaRoute: "/websites",
docsUrl: DOCS_URL,
},
];

Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion libs/portal-plugin-onboarding/src/intents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading
Loading