Skip to content
Closed
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
2 changes: 1 addition & 1 deletion apps/cloud/src/auth/route-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@
export const PUBLIC_PATHS = new Set(["/login"]);

/** Pages an authenticated-but-org-less user is FOR (everything else redirects to onboarding). */
export const ONBOARDING_PATHS = new Set(["/create-org", "/setup-mcp"]);
export const ONBOARDING_PATHS = new Set(["/create-org", "/setup-app", "/setup-mcp"]);
21 changes: 21 additions & 0 deletions apps/cloud/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as CreateOrgRouteImport } from './routes/bare/create-org'
import { Route as LoginRouteImport } from './routes/bare/login'
import { Route as SetupAppRouteImport } from './routes/bare/setup-app'
import { Route as SetupMcpRouteImport } from './routes/bare/setup-mcp'
import { Route as DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIndexRouteImport } from './../../../packages/react/src/routes/index'
import { Route as ApiKeysRouteImport } from './routes/app/api-keys'
Expand Down Expand Up @@ -41,6 +42,11 @@ const LoginRoute = LoginRouteImport.update({
path: '/login',
getParentRoute: () => rootRouteImport,
} as any)
const SetupAppRoute = SetupAppRouteImport.update({
id: '/setup-app',
path: '/setup-app',
getParentRoute: () => rootRouteImport,
} as any)
const SetupMcpRoute = SetupMcpRouteImport.update({
id: '/setup-mcp',
path: '/setup-mcp',
Expand Down Expand Up @@ -166,6 +172,7 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesIntegrationsDotaddDotpluginK
export interface FileRoutesByFullPath {
'/create-org': typeof CreateOrgRoute
'/login': typeof LoginRoute
'/setup-app': typeof SetupAppRoute
'/setup-mcp': typeof SetupMcpRoute
'/{-$orgSlug}/api-keys': typeof ApiKeysRoute
'/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren
Expand All @@ -189,6 +196,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/create-org': typeof CreateOrgRoute
'/login': typeof LoginRoute
'/setup-app': typeof SetupAppRoute
'/setup-mcp': typeof SetupMcpRoute
'/{-$orgSlug}/api-keys': typeof ApiKeysRoute
'/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren
Expand All @@ -213,6 +221,7 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/create-org': typeof CreateOrgRoute
'/login': typeof LoginRoute
'/setup-app': typeof SetupAppRoute
'/setup-mcp': typeof SetupMcpRoute
'/{-$orgSlug}/api-keys': typeof ApiKeysRoute
'/{-$orgSlug}/artifacts': typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren
Expand All @@ -238,6 +247,7 @@ export interface FileRouteTypes {
fullPaths:
| '/create-org'
| '/login'
| '/setup-app'
| '/setup-mcp'
| '/{-$orgSlug}/api-keys'
| '/{-$orgSlug}/artifacts'
Expand All @@ -261,6 +271,7 @@ export interface FileRouteTypes {
to:
| '/create-org'
| '/login'
| '/setup-app'
| '/setup-mcp'
| '/{-$orgSlug}/api-keys'
| '/{-$orgSlug}/artifacts'
Expand All @@ -284,6 +295,7 @@ export interface FileRouteTypes {
| '__root__'
| '/create-org'
| '/login'
| '/setup-app'
| '/setup-mcp'
| '/{-$orgSlug}/api-keys'
| '/{-$orgSlug}/artifacts'
Expand All @@ -308,6 +320,7 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
CreateOrgRoute: typeof CreateOrgRoute
LoginRoute: typeof LoginRoute
SetupAppRoute: typeof SetupAppRoute
SetupMcpRoute: typeof SetupMcpRoute
ApiKeysRoute: typeof ApiKeysRoute
DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute: typeof DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRouteWithChildren
Expand Down Expand Up @@ -343,6 +356,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof LoginRouteImport
parentRoute: typeof rootRouteImport
}
'/setup-app': {
id: '/setup-app'
path: '/setup-app'
fullPath: '/setup-app'
preLoaderRoute: typeof SetupAppRouteImport
parentRoute: typeof rootRouteImport
}
'/setup-mcp': {
id: '/setup-mcp'
path: '/setup-mcp'
Expand Down Expand Up @@ -512,6 +532,7 @@ const DotDotDotDotDotDotDotDotPackagesReactSrcRoutesToolkitsRouteWithChildren =
const rootRouteChildren: RootRouteChildren = {
CreateOrgRoute: CreateOrgRoute,
LoginRoute: LoginRoute,
SetupAppRoute: SetupAppRoute,
SetupMcpRoute: SetupMcpRoute,
ApiKeysRoute: ApiKeysRoute,
DotDotDotDotDotDotDotDotPackagesReactSrcRoutesArtifactsRoute:
Expand Down
7 changes: 7 additions & 0 deletions apps/cloud/src/routes/bare/setup-app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createFileRoute } from "@tanstack/react-router";

import { SetupAppPage } from "../../web/pages/setup-app";

export const Route = createFileRoute("/setup-app")({
component: SetupAppPage,
});
35 changes: 35 additions & 0 deletions apps/cloud/src/web/onboarding-progress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { describe, expect, it } from "@effect/vitest";

import {
readOnboardingPracticeProgress,
writeOnboardingPracticeProgress,
} from "./onboarding-progress";

const makeStorage = () => {
const values = new Map<string, string>();
return {
getItem: (key: string) => values.get(key) ?? null,
setItem: (key: string, value: string) => values.set(key, value),
values,
};
};

describe("onboarding practice progress", () => {
it("persists recognized steps per organization", () => {
const storage = makeStorage();
writeOnboardingPracticeProgress(storage, "acme", new Set(["build_app", "store_notes"]));

expect([...readOnboardingPracticeProgress(storage, "acme")]).toEqual([
"build_app",
"store_notes",
]);
expect([...readOnboardingPracticeProgress(storage, "other")]).toEqual([]);
});

it("ignores malformed or unknown stored values", () => {
const storage = makeStorage();
storage.values.set("executor.onboarding.progress.v1.acme", '["build_app", "unknown", 2]');

expect([...readOnboardingPracticeProgress(storage, "acme")]).toEqual([]);
});
});
47 changes: 47 additions & 0 deletions apps/cloud/src/web/onboarding-progress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export const ONBOARDING_PRACTICE_STEPS = [
"build_app",
"create_workflow",
"create_skill",
"store_notes",
] as const;

export type OnboardingPracticeStep = (typeof ONBOARDING_PRACTICE_STEPS)[number];

type OnboardingStorage = Pick<Storage, "getItem" | "setItem">;

const storageKey = (organizationSlug: string | null): string =>
`executor.onboarding.progress.v1.${organizationSlug ?? "local"}`;

const ProgressSchema = Schema.Array(Schema.Literals(ONBOARDING_PRACTICE_STEPS));
const decodeProgress = Schema.decodeUnknownOption(Schema.fromJsonString(ProgressSchema));

export const readOnboardingPracticeProgress = (
storage: OnboardingStorage | null | undefined,
organizationSlug: string | null,
): ReadonlySet<OnboardingPracticeStep> => {
if (!storage) return new Set();
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: browser storage can throw when disabled
try {
const raw = storage.getItem(storageKey(organizationSlug));
return raw
? new Set(Option.getOrElse(decodeProgress(raw), () => [] as OnboardingPracticeStep[]))
: new Set();
} catch {
return new Set();
}
};

export const writeOnboardingPracticeProgress = (
storage: OnboardingStorage | null | undefined,
organizationSlug: string | null,
progress: ReadonlySet<OnboardingPracticeStep>,
): void => {
if (!storage) return;
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: browser storage can throw when disabled
try {
storage.setItem(storageKey(organizationSlug), JSON.stringify([...progress]));
} catch {
// Storage is optional. The visible state remains useful for this session.
}
};
import { Option, Schema } from "effect";
4 changes: 2 additions & 2 deletions apps/cloud/src/web/pages/create-org.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export const CreateOrgPage = () => {
const form = useCreateOrganizationForm({
defaultName: suggestedName,
onSuccess: () => {
void navigate({ to: "/setup-mcp" });
void navigate({ to: "/setup-app" });
},
});

Expand All @@ -102,7 +102,7 @@ export const CreateOrgPage = () => {
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
<header className="flex flex-col gap-2">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Step 1 of 2
Step 1 of 3
</p>
<h1 className="font-sans text-3xl font-semibold">
{count > 0 ? "You've been invited" : "Create your organization"}
Expand Down
34 changes: 34 additions & 0 deletions apps/cloud/src/web/pages/setup-app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect } from "react";
import { useNavigate } from "@tanstack/react-router";

import { useAuth } from "../auth";

/** Waits for the organization write after creation before entering the scoped app picker. */
export const SetupAppPage = () => {
const auth = useAuth();
const navigate = useNavigate();
const organizationSlug =
auth.status === "authenticated" ? (auth.organization?.slug ?? null) : null;

useEffect(() => {
if (!organizationSlug) return;
void navigate({
to: "/{-$orgSlug}/integrations/browse",
params: { orgSlug: organizationSlug },
search: { onboarding: 1 },
replace: true,
});
}, [navigate, organizationSlug]);

return (
<main className="flex min-h-screen items-center justify-center bg-background px-4">
<section className="w-full max-w-sm">
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Step 2 of 3
</p>
<h1 className="mt-2 font-sans text-3xl font-semibold">Choose your first app</h1>
<p className="mt-2 text-sm text-muted-foreground">Preparing your workspace...</p>
</section>
</main>
);
};
Loading
Loading