Skip to content

Commit

Permalink
feat(onboarding): add shared onboarding guide (#1461)
Browse files Browse the repository at this point in the history
Co-authored-by: José Pereira <7235666+jomifepe@users.noreply.github.com>
  • Loading branch information
BohdanOne and jomifepe authored Oct 21, 2024
1 parent b99f63f commit 50d73dd
Show file tree
Hide file tree
Showing 31 changed files with 789 additions and 174 deletions.
6 changes: 6 additions & 0 deletions packages/manager/src/constants/API_ENDPOINTS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type APIEndpoints = {
PrismicEmbed: string;
PrismicUnsplash: string;
SliceMachineV1: string;
RepositoryService: string;
};

export const API_ENDPOINTS: APIEndpoints = (() => {
Expand All @@ -33,6 +34,9 @@ export const API_ENDPOINTS: APIEndpoints = (() => {
process.env.slice_machine_v1_endpoint ??
"https://mc5qopc07a.execute-api.us-east-1.amazonaws.com/v1/",
),
RepositoryService: addTrailingSlash(
process.env.repository_api ?? "https://repository.wroom.io/",
),
};

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

Expand All @@ -96,6 +101,7 @@ If you didn't intend to run Slice Machine this way, stop it immediately and unse
PrismicEmbed: "https://oembed.prismic.io",
PrismicUnsplash: "https://unsplash.prismic.io/",
SliceMachineV1: "https://sm-api.prismic.io/v1/",
RepositoryService: "https://repository.prismic.io/",
};
}
}
Expand Down
23 changes: 21 additions & 2 deletions packages/manager/src/lib/DecodeError.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,37 @@
import * as t from "io-ts";
import { formatValidationErrors } from "io-ts-reporters";
import { ZodIssue } from "zod";

type DecodeErrorConstructorArgs<TInput = unknown> = {
input: TInput;
errors: t.Errors;
errors: t.Errors | ZodIssue[];
};

function isZodIssueArray(errors: object[]): errors is ZodIssue[] {
return "path" in errors[0];
}

function formatZodErrors(errors: ZodIssue[]): string[] {
return errors.map((err) => {
const path = err.path.length > 0 ? ` at ${err.path.join(".")}` : "";

return `${err.message}${path}`;
});
}

export class DecodeError<TInput = unknown> extends Error {
name = "DecodeError";
input: TInput;
errors: string[];

constructor(args: DecodeErrorConstructorArgs<TInput>) {
const formattedErrors = formatValidationErrors(args.errors);
let formattedErrors: string[] = [];

if (isZodIssueArray(args.errors)) {
formattedErrors = formatZodErrors(args.errors);
} else {
formattedErrors = formatValidationErrors(args.errors);
}

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

Expand Down
34 changes: 30 additions & 4 deletions packages/manager/src/lib/decode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as t from "io-ts";
import { ZodType, ZodTypeDef } from "zod";
import * as E from "fp-ts/Either";
import { pipe } from "fp-ts/function";

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

export const decode = <A, O, I>(
codec: t.Type<A, O, I>,
function isZodSchema(value: unknown): value is ZodType<unknown> {
return (
typeof (value as ZodType<unknown>).safeParse === "function" &&
value instanceof ZodType
);
}

export function decode<A, O, I>(
codec: ZodType<A, ZodTypeDef, unknown>,
input: I,
): DecodeReturnType<A, O, I>;
export function decode<A, O, I>(
codec: t.Type<A, O, I> | ZodType<A, ZodTypeDef, unknown>,
input: I,
): DecodeReturnType<A, O, I>;
export function decode<A, O, I>(
codec: t.Type<A, O, I> | ZodType<A, ZodTypeDef, unknown>,
input: I,
): DecodeReturnType<A, O, I> => {
): DecodeReturnType<A, O, I> {
if (isZodSchema(codec)) {
const parsed = codec.safeParse(input);

if (parsed.success) {
return { value: parsed.data };
}

return { error: new DecodeError({ input, errors: parsed.error.errors }) };
}

return pipe(
codec.decode(input),
E.foldW(
Expand All @@ -33,4 +59,4 @@ export const decode = <A, O, I>(
},
),
);
};
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as t from "io-ts";
import { z } from "zod";
import fetch, { Response } from "../../lib/fetch";
import { fold } from "fp-ts/Either";

Expand Down Expand Up @@ -35,6 +36,7 @@ import {
FrameworkWroomTelemetryID,
StarterId,
Environment,
OnboardingState,
} from "./types";
import { sortEnvironments } from "./sortEnvironments";

Expand Down Expand Up @@ -506,6 +508,115 @@ export class PrismicRepositoryManager extends BaseManager {
}
}

async fetchOnboarding(): Promise<OnboardingState> {
const repositoryName = await this.project.getRepositoryName();

const url = new URL("/onboarding", API_ENDPOINTS.RepositoryService);
url.searchParams.set("repository", repositoryName);
const res = await this._fetch({ url });

if (res.ok) {
const json = await res.json();
const { value, error } = decode(OnboardingState, json);

if (error) {
throw new UnexpectedDataError(
`Failed to decode onboarding: ${error.errors.join(", ")}`,
);
}
if (value) {
return value;
}
}

switch (res.status) {
case 400:
case 401:
throw new UnauthenticatedError();
case 403:
throw new UnauthorizedError();
default:
throw new Error("Failed to fetch onboarding.");
}
}

async toggleOnboardingStep(
stepId: string,
): Promise<{ completedSteps: string[] }> {
const repositoryName = await this.project.getRepositoryName();

const url = new URL(
`/onboarding/${stepId}/toggle`,
API_ENDPOINTS.RepositoryService,
);
url.searchParams.set("repository", repositoryName);
const res = await this._fetch({ url, method: "PATCH" });

if (res.ok) {
const json = await res.json();
const { value, error } = decode(
z.object({ completedSteps: z.array(z.string()) }),
json,
);

if (error) {
throw new UnexpectedDataError(
`Failed to decode onboarding step toggle: ${error.errors.join(", ")}`,
);
}

if (value) {
return value;
}
}

switch (res.status) {
case 400:
case 401:
throw new UnauthenticatedError();
case 403:
throw new UnauthorizedError();
default:
throw new Error("Failed to toggle onboarding step.");
}
}

async toggleOnboarding(): Promise<{ isDismissed: boolean }> {
const repositoryName = await this.project.getRepositoryName();

const url = new URL("/onboarding/toggle", API_ENDPOINTS.RepositoryService);
url.searchParams.set("repository", repositoryName);
const res = await this._fetch({ url, method: "PATCH" });

if (res.ok) {
const json = await res.json();
const { value, error } = decode(
z.object({ isDismissed: z.boolean() }),
json,
);

if (error) {
throw new UnexpectedDataError(
`Failed to decode onboarding toggle: ${error.errors.join(", ")}`,
);
}

if (value) {
return value;
}
}

switch (res.status) {
case 400:
case 401:
throw new UnauthenticatedError();
case 403:
throw new UnauthorizedError();
default:
throw new Error("Failed to toggle onboarding guide.");
}
}

private _decodeLimitOrThrow(
potentialLimit: unknown,
statusCode: number,
Expand All @@ -531,7 +642,7 @@ export class PrismicRepositoryManager extends BaseManager {

private async _fetch(args: {
url: URL;
method?: "GET" | "POST";
method?: "GET" | "POST" | "PATCH";
body?: unknown;
userAgent?: PrismicRepositoryUserAgents;
repository?: string;
Expand Down
38 changes: 38 additions & 0 deletions packages/manager/src/managers/prismicRepository/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
SharedSlice,
} from "@prismicio/types-internal/lib/customtypes";
import * as t from "io-ts";
import { z } from "zod";

export const PrismicRepositoryUserAgent = {
SliceMachine: "prismic-cli/sm",
Expand Down Expand Up @@ -167,3 +168,40 @@ export const Environment = t.type({
),
});
export type Environment = t.TypeOf<typeof Environment>;

export const supportedSliceMachineFrameworks = [
"next",
"nuxt",
"sveltekit",
] as const;

type SupportedFramework = (typeof supportedSliceMachineFrameworks)[number];

function isSupportedFramework(value: string): value is SupportedFramework {
return supportedSliceMachineFrameworks.includes(value as SupportedFramework);
}

export const repositoryFramework = z.preprocess(
(value) => {
// NOTE: we persist a lot of different frameworks in the DB, but only the SM supported are relevant to us
// Any other framework is treated like "other"
if (typeof value === "string" && isSupportedFramework(value)) {
return value;
}

return "other";
},
z.enum([...supportedSliceMachineFrameworks, "other"]),
);

export type RepositoryFramework = z.TypeOf<typeof repositoryFramework>;

export const OnboardingState = z.object({
completedSteps: z.array(z.string()),
isDismissed: z.boolean(),
context: z.object({
framework: repositoryFramework,
starterId: z.string().nullable(),
}),
});
export type OnboardingState = z.infer<typeof OnboardingState>;
Loading

0 comments on commit 50d73dd

Please sign in to comment.