Skip to content

Commit 50d73dd

Browse files
BohdanOnejomifepe
andauthored
feat(onboarding): add shared onboarding guide (#1461)
Co-authored-by: José Pereira <7235666+jomifepe@users.noreply.github.com>
1 parent b99f63f commit 50d73dd

31 files changed

Lines changed: 789 additions & 174 deletions

packages/manager/src/constants/API_ENDPOINTS.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type APIEndpoints = {
1010
PrismicEmbed: string;
1111
PrismicUnsplash: string;
1212
SliceMachineV1: string;
13+
RepositoryService: string;
1314
};
1415

1516
export const API_ENDPOINTS: APIEndpoints = (() => {
@@ -33,6 +34,9 @@ export const API_ENDPOINTS: APIEndpoints = (() => {
3334
process.env.slice_machine_v1_endpoint ??
3435
"https://mc5qopc07a.execute-api.us-east-1.amazonaws.com/v1/",
3536
),
37+
RepositoryService: addTrailingSlash(
38+
process.env.repository_api ?? "https://repository.wroom.io/",
39+
),
3640
};
3741

3842
const missingAPIEndpoints = Object.keys(apiEndpoints).filter((key) => {
@@ -81,6 +85,7 @@ If you didn't intend to run Slice Machine this way, stop it immediately and unse
8185
PrismicUnsplash: "https://unsplash.wroom.io/",
8286
SliceMachineV1:
8387
"https://mc5qopc07a.execute-api.us-east-1.amazonaws.com/v1/",
88+
RepositoryService: "https://repository.wroom.io/",
8489
};
8590
}
8691

@@ -96,6 +101,7 @@ If you didn't intend to run Slice Machine this way, stop it immediately and unse
96101
PrismicEmbed: "https://oembed.prismic.io",
97102
PrismicUnsplash: "https://unsplash.prismic.io/",
98103
SliceMachineV1: "https://sm-api.prismic.io/v1/",
104+
RepositoryService: "https://repository.prismic.io/",
99105
};
100106
}
101107
}

packages/manager/src/lib/DecodeError.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
11
import * as t from "io-ts";
22
import { formatValidationErrors } from "io-ts-reporters";
3+
import { ZodIssue } from "zod";
34

45
type DecodeErrorConstructorArgs<TInput = unknown> = {
56
input: TInput;
6-
errors: t.Errors;
7+
errors: t.Errors | ZodIssue[];
78
};
89

10+
function isZodIssueArray(errors: object[]): errors is ZodIssue[] {
11+
return "path" in errors[0];
12+
}
13+
14+
function formatZodErrors(errors: ZodIssue[]): string[] {
15+
return errors.map((err) => {
16+
const path = err.path.length > 0 ? ` at ${err.path.join(".")}` : "";
17+
18+
return `${err.message}${path}`;
19+
});
20+
}
21+
922
export class DecodeError<TInput = unknown> extends Error {
1023
name = "DecodeError";
1124
input: TInput;
1225
errors: string[];
1326

1427
constructor(args: DecodeErrorConstructorArgs<TInput>) {
15-
const formattedErrors = formatValidationErrors(args.errors);
28+
let formattedErrors: string[] = [];
29+
30+
if (isZodIssueArray(args.errors)) {
31+
formattedErrors = formatZodErrors(args.errors);
32+
} else {
33+
formattedErrors = formatValidationErrors(args.errors);
34+
}
1635

1736
super(formattedErrors.join(", "));
1837

packages/manager/src/lib/decode.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as t from "io-ts";
2+
import { ZodType, ZodTypeDef } from "zod";
23
import * as E from "fp-ts/Either";
34
import { pipe } from "fp-ts/function";
45

@@ -14,10 +15,35 @@ export type DecodeReturnType<A, _O, I> =
1415
error: DecodeError<I>;
1516
};
1617

17-
export const decode = <A, O, I>(
18-
codec: t.Type<A, O, I>,
18+
function isZodSchema(value: unknown): value is ZodType<unknown> {
19+
return (
20+
typeof (value as ZodType<unknown>).safeParse === "function" &&
21+
value instanceof ZodType
22+
);
23+
}
24+
25+
export function decode<A, O, I>(
26+
codec: ZodType<A, ZodTypeDef, unknown>,
27+
input: I,
28+
): DecodeReturnType<A, O, I>;
29+
export function decode<A, O, I>(
30+
codec: t.Type<A, O, I> | ZodType<A, ZodTypeDef, unknown>,
31+
input: I,
32+
): DecodeReturnType<A, O, I>;
33+
export function decode<A, O, I>(
34+
codec: t.Type<A, O, I> | ZodType<A, ZodTypeDef, unknown>,
1935
input: I,
20-
): DecodeReturnType<A, O, I> => {
36+
): DecodeReturnType<A, O, I> {
37+
if (isZodSchema(codec)) {
38+
const parsed = codec.safeParse(input);
39+
40+
if (parsed.success) {
41+
return { value: parsed.data };
42+
}
43+
44+
return { error: new DecodeError({ input, errors: parsed.error.errors }) };
45+
}
46+
2147
return pipe(
2248
codec.decode(input),
2349
E.foldW(
@@ -33,4 +59,4 @@ export const decode = <A, O, I>(
3359
},
3460
),
3561
);
36-
};
62+
}

packages/manager/src/managers/prismicRepository/PrismicRepositoryManager.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as t from "io-ts";
2+
import { z } from "zod";
23
import fetch, { Response } from "../../lib/fetch";
34
import { fold } from "fp-ts/Either";
45

@@ -35,6 +36,7 @@ import {
3536
FrameworkWroomTelemetryID,
3637
StarterId,
3738
Environment,
39+
OnboardingState,
3840
} from "./types";
3941
import { sortEnvironments } from "./sortEnvironments";
4042

@@ -506,6 +508,115 @@ export class PrismicRepositoryManager extends BaseManager {
506508
}
507509
}
508510

511+
async fetchOnboarding(): Promise<OnboardingState> {
512+
const repositoryName = await this.project.getRepositoryName();
513+
514+
const url = new URL("/onboarding", API_ENDPOINTS.RepositoryService);
515+
url.searchParams.set("repository", repositoryName);
516+
const res = await this._fetch({ url });
517+
518+
if (res.ok) {
519+
const json = await res.json();
520+
const { value, error } = decode(OnboardingState, json);
521+
522+
if (error) {
523+
throw new UnexpectedDataError(
524+
`Failed to decode onboarding: ${error.errors.join(", ")}`,
525+
);
526+
}
527+
if (value) {
528+
return value;
529+
}
530+
}
531+
532+
switch (res.status) {
533+
case 400:
534+
case 401:
535+
throw new UnauthenticatedError();
536+
case 403:
537+
throw new UnauthorizedError();
538+
default:
539+
throw new Error("Failed to fetch onboarding.");
540+
}
541+
}
542+
543+
async toggleOnboardingStep(
544+
stepId: string,
545+
): Promise<{ completedSteps: string[] }> {
546+
const repositoryName = await this.project.getRepositoryName();
547+
548+
const url = new URL(
549+
`/onboarding/${stepId}/toggle`,
550+
API_ENDPOINTS.RepositoryService,
551+
);
552+
url.searchParams.set("repository", repositoryName);
553+
const res = await this._fetch({ url, method: "PATCH" });
554+
555+
if (res.ok) {
556+
const json = await res.json();
557+
const { value, error } = decode(
558+
z.object({ completedSteps: z.array(z.string()) }),
559+
json,
560+
);
561+
562+
if (error) {
563+
throw new UnexpectedDataError(
564+
`Failed to decode onboarding step toggle: ${error.errors.join(", ")}`,
565+
);
566+
}
567+
568+
if (value) {
569+
return value;
570+
}
571+
}
572+
573+
switch (res.status) {
574+
case 400:
575+
case 401:
576+
throw new UnauthenticatedError();
577+
case 403:
578+
throw new UnauthorizedError();
579+
default:
580+
throw new Error("Failed to toggle onboarding step.");
581+
}
582+
}
583+
584+
async toggleOnboarding(): Promise<{ isDismissed: boolean }> {
585+
const repositoryName = await this.project.getRepositoryName();
586+
587+
const url = new URL("/onboarding/toggle", API_ENDPOINTS.RepositoryService);
588+
url.searchParams.set("repository", repositoryName);
589+
const res = await this._fetch({ url, method: "PATCH" });
590+
591+
if (res.ok) {
592+
const json = await res.json();
593+
const { value, error } = decode(
594+
z.object({ isDismissed: z.boolean() }),
595+
json,
596+
);
597+
598+
if (error) {
599+
throw new UnexpectedDataError(
600+
`Failed to decode onboarding toggle: ${error.errors.join(", ")}`,
601+
);
602+
}
603+
604+
if (value) {
605+
return value;
606+
}
607+
}
608+
609+
switch (res.status) {
610+
case 400:
611+
case 401:
612+
throw new UnauthenticatedError();
613+
case 403:
614+
throw new UnauthorizedError();
615+
default:
616+
throw new Error("Failed to toggle onboarding guide.");
617+
}
618+
}
619+
509620
private _decodeLimitOrThrow(
510621
potentialLimit: unknown,
511622
statusCode: number,
@@ -531,7 +642,7 @@ export class PrismicRepositoryManager extends BaseManager {
531642

532643
private async _fetch(args: {
533644
url: URL;
534-
method?: "GET" | "POST";
645+
method?: "GET" | "POST" | "PATCH";
535646
body?: unknown;
536647
userAgent?: PrismicRepositoryUserAgents;
537648
repository?: string;

packages/manager/src/managers/prismicRepository/types.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
SharedSlice,
44
} from "@prismicio/types-internal/lib/customtypes";
55
import * as t from "io-ts";
6+
import { z } from "zod";
67

78
export const PrismicRepositoryUserAgent = {
89
SliceMachine: "prismic-cli/sm",
@@ -167,3 +168,40 @@ export const Environment = t.type({
167168
),
168169
});
169170
export type Environment = t.TypeOf<typeof Environment>;
171+
172+
export const supportedSliceMachineFrameworks = [
173+
"next",
174+
"nuxt",
175+
"sveltekit",
176+
] as const;
177+
178+
type SupportedFramework = (typeof supportedSliceMachineFrameworks)[number];
179+
180+
function isSupportedFramework(value: string): value is SupportedFramework {
181+
return supportedSliceMachineFrameworks.includes(value as SupportedFramework);
182+
}
183+
184+
export const repositoryFramework = z.preprocess(
185+
(value) => {
186+
// NOTE: we persist a lot of different frameworks in the DB, but only the SM supported are relevant to us
187+
// Any other framework is treated like "other"
188+
if (typeof value === "string" && isSupportedFramework(value)) {
189+
return value;
190+
}
191+
192+
return "other";
193+
},
194+
z.enum([...supportedSliceMachineFrameworks, "other"]),
195+
);
196+
197+
export type RepositoryFramework = z.TypeOf<typeof repositoryFramework>;
198+
199+
export const OnboardingState = z.object({
200+
completedSteps: z.array(z.string()),
201+
isDismissed: z.boolean(),
202+
context: z.object({
203+
framework: repositoryFramework,
204+
starterId: z.string().nullable(),
205+
}),
206+
});
207+
export type OnboardingState = z.infer<typeof OnboardingState>;

0 commit comments

Comments
 (0)