From 50d73dd6825b4d3793404001a84f022429cdb134 Mon Sep 17 00:00:00 2001 From: Bohdan Imiela <47499119+BohdanOne@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:45:06 +0200 Subject: [PATCH] feat(onboarding): add shared onboarding guide (#1461) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: José Pereira <7235666+jomifepe@users.noreply.github.com> --- .../manager/src/constants/API_ENDPOINTS.ts | 6 + packages/manager/src/lib/DecodeError.ts | 23 ++- packages/manager/src/lib/decode.ts | 34 ++++- .../PrismicRepositoryManager.ts | 113 +++++++++++++- .../src/managers/prismicRepository/types.ts | 38 +++++ .../manager/src/managers/telemetry/types.ts | 59 +++++--- .../test/SliceMachineManager-getState.test.ts | 1 + ...-prismicRepository-fetchOnboarding.test.ts | 143 ++++++++++++++++++ .../__testutils__/mockRepositoryServiceAPI.ts | 112 ++++++++++++++ packages/slice-machine/package.json | 7 +- .../src/components/SideNav/SideNav.module.css | 16 +- .../src/components/SideNav/SideNav.tsx | 30 ++++ .../features/onboarding/OnboardingGuide.tsx | 30 ++++ .../OnboardingGuide/OnboardingGuide.tsx | 101 ------------- .../onboarding/SharedOnboardingGuide.tsx | 37 +++++ .../OnboardingGuide.module.css | 0 .../OnboardingProgressStepper.tsx | 8 +- .../OnboardingProvider.tsx | 8 +- .../OnboardingStepDialog.tsx | 7 +- .../OnboardingStepDialogContent.tsx | 4 +- .../OnboardingStepDialog/index.ts | 0 .../OnboardingTutorial/OnboardingTutorial.tsx | 0 .../SliceMachineOnboardingGuide.tsx | 69 +++++++++ .../content.tsx | 2 +- .../types.ts | 0 .../onboarding/{OnboardingGuide => }/index.ts | 0 .../src/features/onboarding/useOnboarding.ts | 62 ++++++++ .../useSharedOnboardingExperiment.ts | 6 + .../legacy/components/Navigation/index.tsx | 2 +- .../src/modules/__fixtures__/serverState.ts | 1 + yarn.lock | 44 +++--- 31 files changed, 789 insertions(+), 174 deletions(-) create mode 100644 packages/manager/test/SliceMachineManager-prismicRepository-fetchOnboarding.test.ts create mode 100644 packages/manager/test/__testutils__/mockRepositoryServiceAPI.ts create mode 100644 packages/slice-machine/src/features/onboarding/OnboardingGuide.tsx delete mode 100644 packages/slice-machine/src/features/onboarding/OnboardingGuide/OnboardingGuide.tsx create mode 100644 packages/slice-machine/src/features/onboarding/SharedOnboardingGuide.tsx rename packages/slice-machine/src/features/onboarding/{OnboardingGuide => SliceMachineOnboardingGuide}/OnboardingGuide.module.css (100%) rename packages/slice-machine/src/features/onboarding/{ => SliceMachineOnboardingGuide}/OnboardingProgressStepper.tsx (91%) rename packages/slice-machine/src/features/onboarding/{ => SliceMachineOnboardingGuide}/OnboardingProvider.tsx (92%) rename packages/slice-machine/src/features/onboarding/{ => SliceMachineOnboardingGuide}/OnboardingStepDialog/OnboardingStepDialog.tsx (83%) rename packages/slice-machine/src/features/onboarding/{ => SliceMachineOnboardingGuide}/OnboardingStepDialog/OnboardingStepDialogContent.tsx (81%) rename packages/slice-machine/src/features/onboarding/{ => SliceMachineOnboardingGuide}/OnboardingStepDialog/index.ts (100%) rename packages/slice-machine/src/features/onboarding/{ => SliceMachineOnboardingGuide}/OnboardingTutorial/OnboardingTutorial.tsx (100%) create mode 100644 packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/SliceMachineOnboardingGuide.tsx rename packages/slice-machine/src/features/onboarding/{ => SliceMachineOnboardingGuide}/content.tsx (98%) rename packages/slice-machine/src/features/onboarding/{ => SliceMachineOnboardingGuide}/types.ts (100%) rename packages/slice-machine/src/features/onboarding/{OnboardingGuide => }/index.ts (100%) create mode 100644 packages/slice-machine/src/features/onboarding/useOnboarding.ts create mode 100644 packages/slice-machine/src/features/onboarding/useSharedOnboardingExperiment.ts diff --git a/packages/manager/src/constants/API_ENDPOINTS.ts b/packages/manager/src/constants/API_ENDPOINTS.ts index 6e2610c7f6..525fa36ce7 100644 --- a/packages/manager/src/constants/API_ENDPOINTS.ts +++ b/packages/manager/src/constants/API_ENDPOINTS.ts @@ -10,6 +10,7 @@ export type APIEndpoints = { PrismicEmbed: string; PrismicUnsplash: string; SliceMachineV1: string; + RepositoryService: string; }; export const API_ENDPOINTS: APIEndpoints = (() => { @@ -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) => { @@ -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/", }; } @@ -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/", }; } } diff --git a/packages/manager/src/lib/DecodeError.ts b/packages/manager/src/lib/DecodeError.ts index e06013239f..f3315b0795 100644 --- a/packages/manager/src/lib/DecodeError.ts +++ b/packages/manager/src/lib/DecodeError.ts @@ -1,18 +1,37 @@ import * as t from "io-ts"; import { formatValidationErrors } from "io-ts-reporters"; +import { ZodIssue } from "zod"; type DecodeErrorConstructorArgs = { 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 extends Error { name = "DecodeError"; input: TInput; errors: string[]; constructor(args: DecodeErrorConstructorArgs) { - const formattedErrors = formatValidationErrors(args.errors); + let formattedErrors: string[] = []; + + if (isZodIssueArray(args.errors)) { + formattedErrors = formatZodErrors(args.errors); + } else { + formattedErrors = formatValidationErrors(args.errors); + } super(formattedErrors.join(", ")); diff --git a/packages/manager/src/lib/decode.ts b/packages/manager/src/lib/decode.ts index 92f6e76b74..c6861fafe1 100644 --- a/packages/manager/src/lib/decode.ts +++ b/packages/manager/src/lib/decode.ts @@ -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"; @@ -14,10 +15,35 @@ export type DecodeReturnType = error: DecodeError; }; -export const decode = ( - codec: t.Type, +function isZodSchema(value: unknown): value is ZodType { + return ( + typeof (value as ZodType).safeParse === "function" && + value instanceof ZodType + ); +} + +export function decode( + codec: ZodType, + input: I, +): DecodeReturnType; +export function decode( + codec: t.Type | ZodType, + input: I, +): DecodeReturnType; +export function decode( + codec: t.Type | ZodType, input: I, -): DecodeReturnType => { +): DecodeReturnType { + 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( @@ -33,4 +59,4 @@ export const decode = ( }, ), ); -}; +} diff --git a/packages/manager/src/managers/prismicRepository/PrismicRepositoryManager.ts b/packages/manager/src/managers/prismicRepository/PrismicRepositoryManager.ts index badf117b1a..8684e5f347 100644 --- a/packages/manager/src/managers/prismicRepository/PrismicRepositoryManager.ts +++ b/packages/manager/src/managers/prismicRepository/PrismicRepositoryManager.ts @@ -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"; @@ -35,6 +36,7 @@ import { FrameworkWroomTelemetryID, StarterId, Environment, + OnboardingState, } from "./types"; import { sortEnvironments } from "./sortEnvironments"; @@ -506,6 +508,115 @@ export class PrismicRepositoryManager extends BaseManager { } } + async fetchOnboarding(): Promise { + 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, @@ -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; diff --git a/packages/manager/src/managers/prismicRepository/types.ts b/packages/manager/src/managers/prismicRepository/types.ts index 5ffca8c79c..a90d7030fd 100644 --- a/packages/manager/src/managers/prismicRepository/types.ts +++ b/packages/manager/src/managers/prismicRepository/types.ts @@ -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", @@ -167,3 +168,40 @@ export const Environment = t.type({ ), }); export type Environment = t.TypeOf; + +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; + +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; diff --git a/packages/manager/src/managers/telemetry/types.ts b/packages/manager/src/managers/telemetry/types.ts index 665e985586..0127b940a1 100644 --- a/packages/manager/src/managers/telemetry/types.ts +++ b/packages/manager/src/managers/telemetry/types.ts @@ -35,13 +35,16 @@ export const SegmentEventType = { sliceMachine_start: "slice-machine:start", sliceLibrary_beta_modalOpened: "slice-library:beta:modal-opened", sliceLibrary_beta_codeOpened: "slice-library:beta:code-opened", - onboarding_step_opened: "onboarding:step-opened", - onboarding_step_completed: "onboarding:step-completed", - onboarding_completed: "onboarding:completed", postPush_emptyStateCtaClicked: "post-push:empty-state-cta-clicked", postPush_toastCtaClicked: "post-push:toast-cta-clicked", experiment_exposure: "experiment:exposure", sliceName_pascalCaseError: "slice-name-error:pascal-case", + onboarding_step_opened: "onboarding:step-opened", + onboarding_step_completed: "onboarding:step-completed", + onboarding_completed: "onboarding:completed", + sharedOnboarding_step_opened: "shared-onboarding:step-opened", + sharedOnboarding_step_completed: "shared-onboarding:step-completed", + sharedOnboarding_completed: "shared-onboarding:completed", } as const; type SegmentEventTypes = (typeof SegmentEventType)[keyof typeof SegmentEventType]; @@ -87,11 +90,6 @@ export const HumanSegmentEventType = { "SliceMachine Slice Library [BETA] CTA modal displayed", [SegmentEventType.sliceLibrary_beta_codeOpened]: "SliceMachine Slice Library [BETA] CTA example code opened", - [SegmentEventType.onboarding_step_opened]: - "SliceMachine Onboarding Step Opened", - [SegmentEventType.onboarding_step_completed]: - "SliceMachine Onboarding Step Completed", - [SegmentEventType.onboarding_completed]: "SliceMachine Onboarding Completed", [SegmentEventType.postPush_emptyStateCtaClicked]: "SliceMachine Post Push Empty State CTA Clicked", [SegmentEventType.postPush_toastCtaClicked]: @@ -99,7 +97,19 @@ export const HumanSegmentEventType = { [SegmentEventType.experiment_exposure]: "$exposure", [SegmentEventType.sliceName_pascalCaseError]: "SliceMachine Slice Name Pascal Case Error", + [SegmentEventType.onboarding_step_opened]: + "SliceMachine Onboarding Step Opened", + [SegmentEventType.onboarding_step_completed]: + "SliceMachine Onboarding Step Completed", + [SegmentEventType.onboarding_completed]: "SliceMachine Onboarding Completed", + [SegmentEventType.sharedOnboarding_step_completed]: + "Prismic Onboarding Guide Step Completed", + [SegmentEventType.sharedOnboarding_step_opened]: + "Prismic Onboarding Guide Step Open", + [SegmentEventType.sharedOnboarding_completed]: + "Prismic Onboarding Guide Completed", } as const; + export type HumanSegmentEventTypes = (typeof HumanSegmentEventType)[keyof typeof HumanSegmentEventType]; @@ -338,26 +348,36 @@ type SliceLibraryBetaCodeOpened = SegmentEvent< typeof SegmentEventType.sliceLibrary_beta_codeOpened >; +type OnboardingCommonPayload = { stepId: string; stepTitle: string }; +type SharedOnboardingProperties> = T & { + source: "SliceMachine"; +}; + type SliceMachineOnboardingStepOpened = SegmentEvent< typeof SegmentEventType.onboarding_step_opened, - { - stepId: string; - stepTitle: string; - } + OnboardingCommonPayload >; - type SliceMachineOnboardingStepCompleted = SegmentEvent< typeof SegmentEventType.onboarding_step_completed, - { - stepId: string; - stepTitle: string; - } + OnboardingCommonPayload >; - type SliceMachineOnboardingCompleted = SegmentEvent< typeof SegmentEventType.onboarding_completed >; +type SliceMachineSharedOnboardingStepOpened = SegmentEvent< + typeof SegmentEventType.sharedOnboarding_step_opened, + SharedOnboardingProperties +>; +type SliceMachineSharedOnboardingStepCompleted = SegmentEvent< + typeof SegmentEventType.sharedOnboarding_step_completed, + SharedOnboardingProperties +>; +type SliceMachineSharedOnboardingCompleted = SegmentEvent< + typeof SegmentEventType.sharedOnboarding_completed, + SharedOnboardingProperties +>; + type SliceMachinePostPushEmptyStateCtaClicked = SegmentEvent< typeof SegmentEventType.postPush_emptyStateCtaClicked >; @@ -412,6 +432,9 @@ export type SegmentEvents = | SliceMachineOnboardingStepOpened | SliceMachineOnboardingStepCompleted | SliceMachineOnboardingCompleted + | SliceMachineSharedOnboardingStepOpened + | SliceMachineSharedOnboardingStepCompleted + | SliceMachineSharedOnboardingCompleted | SliceMachinePostPushEmptyStateCtaClicked | SliceMachinePostPushToastCtaClicked | SliceMachineExperimentExposure diff --git a/packages/manager/test/SliceMachineManager-getState.test.ts b/packages/manager/test/SliceMachineManager-getState.test.ts index 2a4a1f7e52..5fe5750d34 100644 --- a/packages/manager/test/SliceMachineManager-getState.test.ts +++ b/packages/manager/test/SliceMachineManager-getState.test.ts @@ -26,6 +26,7 @@ it("returns global Slice Machine state", async () => { PrismicWroom: "https://prismic.io/", PrismicUnsplash: "https://unsplash.prismic.io/", SliceMachineV1: "https://sm-api.prismic.io/v1/", + RepositoryService: "https://repository.prismic.io/", }); expect(result.clientError).toStrictEqual({ message: "__stub__", diff --git a/packages/manager/test/SliceMachineManager-prismicRepository-fetchOnboarding.test.ts b/packages/manager/test/SliceMachineManager-prismicRepository-fetchOnboarding.test.ts new file mode 100644 index 0000000000..d8aed1c7f4 --- /dev/null +++ b/packages/manager/test/SliceMachineManager-prismicRepository-fetchOnboarding.test.ts @@ -0,0 +1,143 @@ +import { expect, it } from "vitest"; + +import { createPrismicAuthLoginResponse } from "./__testutils__/createPrismicAuthLoginResponse"; +import { createTestPlugin } from "./__testutils__/createTestPlugin"; +import { createTestProject } from "./__testutils__/createTestProject"; + +import { UnauthenticatedError, createSliceMachineManager } from "../src"; +import { mockRepositoryServiceAPI } from "./__testutils__/mockRepositoryServiceAPI"; +import { mockPrismicAuthAPI } from "./__testutils__/mockPrismicAuthAPI"; + +it("returns onboarding state for the logged in user", async (ctx) => { + const adapter = createTestPlugin(); + const cwd = await createTestProject({ adapter }); + const manager = createSliceMachineManager({ + nativePlugins: { [adapter.meta.name]: adapter }, + cwd, + }); + + mockPrismicAuthAPI(ctx); + + const prismicAuthLoginResponse = createPrismicAuthLoginResponse(); + await manager.user.login(prismicAuthLoginResponse); + const authenticationToken = await manager.user.getAuthenticationToken(); + + const steps = ["reviewAndPush", "codePage", "createContent"]; + mockRepositoryServiceAPI(ctx, { + fetchEndpoint: { + steps, + expectedAuthenticationToken: authenticationToken, + expectedCookies: prismicAuthLoginResponse.cookies, + }, + }); + + const res = await manager.prismicRepository.fetchOnboarding(); + + expect(res).toStrictEqual({ + completedSteps: steps, + isDismissed: false, + context: { + framework: "next", + starterId: null, + }, + }); +}); + +it("toggles an onboarding step and returns the new completedSteps", async (ctx) => { + const adapter = createTestPlugin(); + const cwd = await createTestProject({ adapter }); + const manager = createSliceMachineManager({ + nativePlugins: { [adapter.meta.name]: adapter }, + cwd, + }); + + mockPrismicAuthAPI(ctx); + + const prismicAuthLoginResponse = createPrismicAuthLoginResponse(); + await manager.user.login(prismicAuthLoginResponse); + const authenticationToken = await manager.user.getAuthenticationToken(); + + mockRepositoryServiceAPI(ctx, { + toggleStepEndpoint: { + steps: ["reviewAndPush", "codePage", "createContent"], + expectedAuthenticationToken: authenticationToken, + expectedCookies: prismicAuthLoginResponse.cookies, + }, + }); + + const res = + await manager.prismicRepository.toggleOnboardingStep("reviewAndPush"); + + expect(res).toStrictEqual({ completedSteps: ["codePage", "createContent"] }); +}); + +it("toggles onboarding", async (ctx) => { + const adapter = createTestPlugin(); + const cwd = await createTestProject({ adapter }); + const manager = createSliceMachineManager({ + nativePlugins: { [adapter.meta.name]: adapter }, + cwd, + }); + + mockPrismicAuthAPI(ctx); + + const prismicAuthLoginResponse = createPrismicAuthLoginResponse(); + await manager.user.login(prismicAuthLoginResponse); + const authenticationToken = await manager.user.getAuthenticationToken(); + + mockRepositoryServiceAPI(ctx, { + toggleEndpoint: { + isDismissed: true, + expectedAuthenticationToken: authenticationToken, + expectedCookies: prismicAuthLoginResponse.cookies, + }, + }); + + const res = await manager.prismicRepository.toggleOnboarding(); + + expect(res).toStrictEqual({ isDismissed: true }); +}); + +it("throws if the API response was invalid", async (ctx) => { + const adapter = createTestPlugin(); + const cwd = await createTestProject({ adapter }); + const manager = createSliceMachineManager({ + nativePlugins: { [adapter.meta.name]: adapter }, + cwd, + }); + + mockPrismicAuthAPI(ctx); + + const prismicAuthLoginResponse = createPrismicAuthLoginResponse(); + await manager.user.login(prismicAuthLoginResponse); + + const authenticationToken = await manager.user.getAuthenticationToken(); + + mockRepositoryServiceAPI(ctx, { + fetchEndpoint: { + steps: [], + invalid: true, + expectedAuthenticationToken: authenticationToken, + expectedCookies: prismicAuthLoginResponse.cookies, + }, + }); + + await expect(async () => { + await manager.prismicRepository.fetchOnboarding(); + }).rejects.toThrow(/Failed to decode onboarding/i); +}); + +it("throws if not logged in", async () => { + const adapter = createTestPlugin(); + const cwd = await createTestProject({ adapter }); + const manager = createSliceMachineManager({ + nativePlugins: { [adapter.meta.name]: adapter }, + cwd, + }); + + await manager.user.logout(); + + await expect(async () => { + await manager.prismicRepository.fetchOnboarding(); + }).rejects.toThrow(UnauthenticatedError); +}); diff --git a/packages/manager/test/__testutils__/mockRepositoryServiceAPI.ts b/packages/manager/test/__testutils__/mockRepositoryServiceAPI.ts new file mode 100644 index 0000000000..317ffb6508 --- /dev/null +++ b/packages/manager/test/__testutils__/mockRepositoryServiceAPI.ts @@ -0,0 +1,112 @@ +import { TestContext } from "vitest"; +import { PathParams, rest, RestRequest } from "msw"; + +type MockRepositoryServiceAPIConfig = { + endpoint?: string; + fetchEndpoint?: { + steps: string[]; + expectedAuthenticationToken: string; + expectedCookies: string[]; + invalid?: boolean; + }; + toggleStepEndpoint?: { + steps: string[]; + expectedAuthenticationToken: string; + expectedCookies: string[]; + }; + toggleEndpoint?: { + isDismissed: boolean; + expectedAuthenticationToken: string; + expectedCookies: string[]; + }; +}; + +export const mockRepositoryServiceAPI = ( + ctx: TestContext, + config: MockRepositoryServiceAPIConfig, +): void => { + const endpoint = config?.endpoint ?? "https://repository.prismic.io/"; + + const checkAuth = ( + expected: { + expectedAuthenticationToken: string; + expectedCookies: string[]; + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + req: RestRequest>, + ) => { + return ( + req.headers.get("Authorization") === + `Bearer ${expected.expectedAuthenticationToken}` && + req.headers.get("Cookie") === expected.expectedCookies.join("; ") + ); + }; + + ctx.msw.use( + rest.get(new URL("onboarding", endpoint).toString(), (req, res, ctx) => { + if (!config.fetchEndpoint) { + return res(ctx.status(418)); + } + + if (checkAuth(config.fetchEndpoint, req)) { + return res( + ctx.json({ + completedSteps: config.fetchEndpoint.invalid + ? "somethingElse" + : config.fetchEndpoint.steps, + isDismissed: false, + context: { + framework: "next", + starterId: null, + }, + }), + ctx.status(200), + ); + } + + return res(ctx.status(418)); + }), + ); + + ctx.msw.use( + rest.patch( + new URL("onboarding/reviewAndPush/toggle", endpoint).toString(), + (req, res, ctx) => { + if (!config.toggleStepEndpoint) { + return res(ctx.status(418)); + } + + if (checkAuth(config.toggleStepEndpoint, req)) { + return res( + ctx.json({ + completedSteps: config.toggleStepEndpoint.steps.filter( + (step) => step !== "reviewAndPush", + ), + }), + ); + } + + return res(ctx.status(418)); + }, + ), + ); + + ctx.msw.use( + rest.patch( + new URL("onboarding/toggle", endpoint).toString(), + (req, res, ctx) => { + if (!config.toggleEndpoint) { + return res(ctx.status(418)); + } + + if (checkAuth(config.toggleEndpoint, req)) { + return res( + ctx.json({ isDismissed: config.toggleEndpoint.isDismissed }), + ); + } + + return res(ctx.status(418)); + }, + ), + ); +}; diff --git a/packages/slice-machine/package.json b/packages/slice-machine/package.json index 2f45b47513..a7f1074032 100644 --- a/packages/slice-machine/package.json +++ b/packages/slice-machine/package.json @@ -43,9 +43,9 @@ "@emotion/react": "11.11.1", "@extractus/oembed-extractor": "3.1.8", "@prismicio/client": "7.11.0", - "@prismicio/editor-fields": "0.4.49", - "@prismicio/editor-support": "0.4.49", - "@prismicio/editor-ui": "0.4.49", + "@prismicio/editor-fields": "0.4.51", + "@prismicio/editor-support": "0.4.51", + "@prismicio/editor-ui": "0.4.51", "@prismicio/mock": "0.3.3", "@prismicio/mocks": "2.4.0", "@prismicio/simulator": "0.1.4", @@ -77,7 +77,6 @@ "connected-next-router": "4.2.0", "cross-env": "7.0.3", "depcheck": "1.4.3", - "dom-confetti": "0.2.2", "eslint": "8.56.0", "eslint-config-prettier": "9.0.0", "eslint-import-resolver-typescript": "3.5.3", diff --git a/packages/slice-machine/src/components/SideNav/SideNav.module.css b/packages/slice-machine/src/components/SideNav/SideNav.module.css index cccef10a4d..9d635fe803 100644 --- a/packages/slice-machine/src/components/SideNav/SideNav.module.css +++ b/packages/slice-machine/src/components/SideNav/SideNav.module.css @@ -130,7 +130,12 @@ margin-top: 8px; } -.link { +.button { + width: 100%; +} + +.link, +.button { composes: block interactiveElement; align-items: center; border-radius: 6px; @@ -180,7 +185,8 @@ } } -.linkIcon { +.linkIcon, +.buttonIcon { composes: blockWithDisplayRevert; /* Not Active */ .link:not([data-active="true"]) & { @@ -192,14 +198,16 @@ } } -.linkContent { +.linkContent, +.buttonContent { composes: block; display: flex; justify-content: space-between; width: 100%; } -.linkText { +.linkText, +.buttonText { composes: blockWithDisplayRevert; align-self: baseline; } diff --git a/packages/slice-machine/src/components/SideNav/SideNav.tsx b/packages/slice-machine/src/components/SideNav/SideNav.tsx index 2dcfcdba94..4030397d23 100644 --- a/packages/slice-machine/src/components/SideNav/SideNav.tsx +++ b/packages/slice-machine/src/components/SideNav/SideNav.tsx @@ -132,6 +132,36 @@ export const SideNavLink: FC = ({ ); }; +export type SideNavButtonProps = { + title: string; + active?: boolean; + Icon: FC>; + RightElement?: ReactNode; + onClick?: (event: MouseEvent) => void; +}; + +export const SideNavButton: FC = ({ + title, + RightElement, + Icon, + active, + ...otherProps +}) => { + const visible = useMediaQuery({ max: "medium" }); + + return ( + + + + ); +}; + type RightElementProps = PropsWithChildren< { type?: "pill" | "text"; diff --git a/packages/slice-machine/src/features/onboarding/OnboardingGuide.tsx b/packages/slice-machine/src/features/onboarding/OnboardingGuide.tsx new file mode 100644 index 0000000000..39459f6257 --- /dev/null +++ b/packages/slice-machine/src/features/onboarding/OnboardingGuide.tsx @@ -0,0 +1,30 @@ +import { useMediaQuery } from "@prismicio/editor-ui"; + +import { useOnboardingExperiment } from "@/features/onboarding/useOnboardingExperiment"; +import { useUpdateAvailable } from "@/hooks/useUpdateAvailable"; + +import { SharedOnboardingGuide } from "./SharedOnboardingGuide"; +import { SliceMachineOnboardingGuide } from "./SliceMachineOnboardingGuide/SliceMachineOnboardingGuide"; +import { useSharedOnboardingExperiment } from "./useSharedOnboardingExperiment"; + +export function OnboardingGuide() { + const isVisible = useIsOnboardingGuideVisible(); + const isSharedExperimentEligible = useSharedOnboardingExperiment().eligible; + + if (!isVisible) return null; + if (isSharedExperimentEligible) return ; + return ; +} + +function useIsOnboardingGuideVisible() { + const isMediaQueryVisible = useMediaQuery({ min: "medium" }); + const isExperimentEligible = useOnboardingExperiment().eligible; + const updates = useUpdateAvailable(); + + return ( + isMediaQueryVisible && + isExperimentEligible && + !updates.sliceMachineUpdateAvailable && + !updates.adapterUpdateAvailable + ); +} diff --git a/packages/slice-machine/src/features/onboarding/OnboardingGuide/OnboardingGuide.tsx b/packages/slice-machine/src/features/onboarding/OnboardingGuide/OnboardingGuide.tsx deleted file mode 100644 index 705590846a..0000000000 --- a/packages/slice-machine/src/features/onboarding/OnboardingGuide/OnboardingGuide.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { - Card, - CardContent, - ProgressBar, - Text, - useMediaQuery, -} from "@prismicio/editor-ui"; -import { confetti as fireConfetti, ConfettiConfig } from "dom-confetti"; -import { useRef, useState } from "react"; - -import { OnboardingProgressStepper } from "@/features/onboarding/OnboardingProgressStepper"; -import { - OnboardingProvider, - useOnboardingContext, -} from "@/features/onboarding/OnboardingProvider"; -import { OnboardingTutorial } from "@/features/onboarding/OnboardingTutorial/OnboardingTutorial"; -import { useOnboardingExperiment } from "@/features/onboarding/useOnboardingExperiment"; -import { useUpdateAvailable } from "@/hooks/useUpdateAvailable"; - -import styles from "./OnboardingGuide.module.css"; - -export function OnboardingGuide() { - const { eligible } = useOnboardingExperiment(); - const { sliceMachineUpdateAvailable, adapterUpdateAvailable } = - useUpdateAvailable(); - - if (!eligible || sliceMachineUpdateAvailable || adapterUpdateAvailable) { - return null; - } - - return ; -} - -function OnboardingGuideContent() { - const [isVisible, setVisible] = useState(true); - const confettiCannonRef = useRef(null); - - const onComplete = () => { - const { current: confettiCannon } = confettiCannonRef; - if (confettiCannon) fireConfetti(confettiCannon, confettiConfig); - setTimeout(() => setVisible(false), confettiConfig.duration); - }; - - if (!isVisible) return null; - - return ( - -
- -
-
- - ); -} - -function OnboardingGuideCard() { - const { steps, completedStepCount, isComplete } = useOnboardingContext(); - const isVisible = useMediaQuery({ min: "medium" }); - - if (!isVisible) return null; - - return ( -
- - -
- - {`Build your first Prismic Page in ${steps.length.toString()} simple steps`} - - - Render a live page with content coming from Prismic in 5 mins - -
- `${value}/${max}`} - /> - - -
-
-
- ); -} - -const confettiConfig: ConfettiConfig = { - colors: ["#8E44EC", "#E8C7FF", "#59B5F8", "#C3EEFE"], - elementCount: 300, - width: "8px", - height: "8px", - stagger: 0.2, - startVelocity: 35, - spread: 90, - duration: 3000, -}; diff --git a/packages/slice-machine/src/features/onboarding/SharedOnboardingGuide.tsx b/packages/slice-machine/src/features/onboarding/SharedOnboardingGuide.tsx new file mode 100644 index 0000000000..b1fefaf2cd --- /dev/null +++ b/packages/slice-machine/src/features/onboarding/SharedOnboardingGuide.tsx @@ -0,0 +1,37 @@ +import { OnboardingGuide } from "@prismicio/editor-fields"; + +import { telemetry } from "@/apiClient"; +import { SideNavButton, SideNavListItem } from "@/components/SideNav"; +import { PlayCircleIcon } from "@/icons/PlayCircleIcon"; + +import { useOnboarding } from "./useOnboarding"; + +export function SharedOnboardingGuide() { + const { onboarding, toggleStep, toggleGuide } = useOnboarding(); + + if (!onboarding) return null; + + if (onboarding.isDismissed) { + return ( + + void toggleGuide()} + /> + + ); + } + + return ( + telemetry.track({ ...args, source: "SliceMachine" }), + source: "SliceMachine", + }} + onboardingState={onboarding} + onToggleStep={toggleStep} + onToggleGuide={toggleGuide} + /> + ); +} diff --git a/packages/slice-machine/src/features/onboarding/OnboardingGuide/OnboardingGuide.module.css b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingGuide.module.css similarity index 100% rename from packages/slice-machine/src/features/onboarding/OnboardingGuide/OnboardingGuide.module.css rename to packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingGuide.module.css diff --git a/packages/slice-machine/src/features/onboarding/OnboardingProgressStepper.tsx b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingProgressStepper.tsx similarity index 91% rename from packages/slice-machine/src/features/onboarding/OnboardingProgressStepper.tsx rename to packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingProgressStepper.tsx index a05d840e13..2a49a422d3 100644 --- a/packages/slice-machine/src/features/onboarding/OnboardingProgressStepper.tsx +++ b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingProgressStepper.tsx @@ -11,9 +11,10 @@ import { import { useState } from "react"; import { telemetry } from "@/apiClient"; -import { useOnboardingContext } from "@/features/onboarding/OnboardingProvider"; -import { OnboardingStepDialog } from "@/features/onboarding/OnboardingStepDialog"; -import type { OnboardingStep } from "@/features/onboarding/types"; + +import { useOnboardingContext } from "./OnboardingProvider"; +import { OnboardingStepDialog } from "./OnboardingStepDialog"; +import type { OnboardingStep } from "./types"; const EndCtaIcon = () => ; @@ -56,7 +57,6 @@ export function OnboardingProgressStepper( color="grey" sx={{ width: "100%" }} renderEndIcon={EndCtaIcon} - // TODO: Fix typescript error {...{ onMouseEnter: () => setListOpen(true) }} > {completedStepCount > 0 ? "Continue" : "Start now"} diff --git a/packages/slice-machine/src/features/onboarding/OnboardingProvider.tsx b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingProvider.tsx similarity index 92% rename from packages/slice-machine/src/features/onboarding/OnboardingProvider.tsx rename to packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingProvider.tsx index 7aa0e72610..0fa23fc7e5 100644 --- a/packages/slice-machine/src/features/onboarding/OnboardingProvider.tsx +++ b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingProvider.tsx @@ -1,13 +1,13 @@ import { createContext, ReactNode, useContext } from "react"; import { telemetry } from "@/apiClient"; -import { onboardingSteps } from "@/features/onboarding/content"; +import { onboardingSteps } from "@/features/onboarding/SliceMachineOnboardingGuide/content"; import { type OnboardingStep, type OnboardingStepId, type OnboardingStepStatuses, onboardingStepStatusesSchema, -} from "@/features/onboarding/types"; +} from "@/features/onboarding/SliceMachineOnboardingGuide/types"; import { usePersistedState } from "@/hooks/usePersistedState"; type OnboardingContext = { @@ -67,7 +67,9 @@ export const OnboardingProvider = ({ } if (Object.values(nextState).every(Boolean)) { onComplete?.(); - void telemetry.track({ event: "onboarding:completed" }); + void telemetry.track({ + event: "onboarding:completed", + }); } }; diff --git a/packages/slice-machine/src/features/onboarding/OnboardingStepDialog/OnboardingStepDialog.tsx b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingStepDialog/OnboardingStepDialog.tsx similarity index 83% rename from packages/slice-machine/src/features/onboarding/OnboardingStepDialog/OnboardingStepDialog.tsx rename to packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingStepDialog/OnboardingStepDialog.tsx index d47722027b..eb62c47d92 100644 --- a/packages/slice-machine/src/features/onboarding/OnboardingStepDialog/OnboardingStepDialog.tsx +++ b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingStepDialog/OnboardingStepDialog.tsx @@ -8,9 +8,10 @@ import { } from "@prismicio/editor-ui"; import { useState } from "react"; -import { useOnboardingContext } from "@/features/onboarding/OnboardingProvider"; -import { OnboardingStepDialogContent } from "@/features/onboarding/OnboardingStepDialog/OnboardingStepDialogContent"; -import type { OnboardingStep } from "@/features/onboarding/types"; +import { useOnboardingContext } from "@/features/onboarding/SliceMachineOnboardingGuide/OnboardingProvider"; +import type { OnboardingStep } from "@/features/onboarding/SliceMachineOnboardingGuide/types"; + +import { OnboardingStepDialogContent } from "./OnboardingStepDialogContent"; type OnboardingStepDialogProps = { step: OnboardingStep; diff --git a/packages/slice-machine/src/features/onboarding/OnboardingStepDialog/OnboardingStepDialogContent.tsx b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingStepDialog/OnboardingStepDialogContent.tsx similarity index 81% rename from packages/slice-machine/src/features/onboarding/OnboardingStepDialog/OnboardingStepDialogContent.tsx rename to packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingStepDialog/OnboardingStepDialogContent.tsx index 7c5b03267d..10516a40d1 100644 --- a/packages/slice-machine/src/features/onboarding/OnboardingStepDialog/OnboardingStepDialogContent.tsx +++ b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingStepDialog/OnboardingStepDialogContent.tsx @@ -1,7 +1,7 @@ import { Box, ScrollArea, Text, Video } from "@prismicio/editor-ui"; -import { useOnboardingContext } from "@/features/onboarding/OnboardingProvider"; -import { OnboardingStep } from "@/features/onboarding/types"; +import { useOnboardingContext } from "@/features/onboarding/SliceMachineOnboardingGuide/OnboardingProvider"; +import { OnboardingStep } from "@/features/onboarding/SliceMachineOnboardingGuide/types"; type OnboardingStepDialogContentProps = { step: OnboardingStep; diff --git a/packages/slice-machine/src/features/onboarding/OnboardingStepDialog/index.ts b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingStepDialog/index.ts similarity index 100% rename from packages/slice-machine/src/features/onboarding/OnboardingStepDialog/index.ts rename to packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingStepDialog/index.ts diff --git a/packages/slice-machine/src/features/onboarding/OnboardingTutorial/OnboardingTutorial.tsx b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingTutorial/OnboardingTutorial.tsx similarity index 100% rename from packages/slice-machine/src/features/onboarding/OnboardingTutorial/OnboardingTutorial.tsx rename to packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/OnboardingTutorial/OnboardingTutorial.tsx diff --git a/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/SliceMachineOnboardingGuide.tsx b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/SliceMachineOnboardingGuide.tsx new file mode 100644 index 0000000000..943249b67b --- /dev/null +++ b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/SliceMachineOnboardingGuide.tsx @@ -0,0 +1,69 @@ +import { useConfetti } from "@prismicio/editor-support/Animation"; +import { + Card, + CardContent, + ProgressBar, + Text, + useMediaQuery, +} from "@prismicio/editor-ui"; +import { useState } from "react"; + +import styles from "./OnboardingGuide.module.css"; +import { OnboardingProgressStepper } from "./OnboardingProgressStepper"; +import { OnboardingProvider, useOnboardingContext } from "./OnboardingProvider"; +import { OnboardingTutorial } from "./OnboardingTutorial/OnboardingTutorial"; + +export function SliceMachineOnboardingGuide() { + const [isVisible, setVisible] = useState(true); + const confetti = useConfetti({ onAnimationEnd: () => setVisible(false) }); + + if (!isVisible) return null; + + return ( + +
+ +
+
+ + ); +} + +function OnboardingGuideCard() { + const { steps, completedStepCount, isComplete } = useOnboardingContext(); + const isVisible = useMediaQuery({ min: "medium" }); + + if (!isVisible) return null; + + return ( +
+ + +
+ + {`Build your first Prismic Page in ${steps.length.toString()} simple steps`} + + + Render a live page with content coming from Prismic in 5 mins + +
+ `${value}/${max}`} + /> + + +
+
+
+ ); +} diff --git a/packages/slice-machine/src/features/onboarding/content.tsx b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/content.tsx similarity index 98% rename from packages/slice-machine/src/features/onboarding/content.tsx rename to packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/content.tsx index 9f261ff42b..ac71b31e61 100644 --- a/packages/slice-machine/src/features/onboarding/content.tsx +++ b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/content.tsx @@ -1,6 +1,6 @@ import { Text } from "@prismicio/editor-ui"; -import type { OnboardingStep } from "@/features/onboarding/types"; +import type { OnboardingStep } from "./types"; export const onboardingSteps: OnboardingStep[] = [ { diff --git a/packages/slice-machine/src/features/onboarding/types.ts b/packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/types.ts similarity index 100% rename from packages/slice-machine/src/features/onboarding/types.ts rename to packages/slice-machine/src/features/onboarding/SliceMachineOnboardingGuide/types.ts diff --git a/packages/slice-machine/src/features/onboarding/OnboardingGuide/index.ts b/packages/slice-machine/src/features/onboarding/index.ts similarity index 100% rename from packages/slice-machine/src/features/onboarding/OnboardingGuide/index.ts rename to packages/slice-machine/src/features/onboarding/index.ts diff --git a/packages/slice-machine/src/features/onboarding/useOnboarding.ts b/packages/slice-machine/src/features/onboarding/useOnboarding.ts new file mode 100644 index 0000000000..1975faee73 --- /dev/null +++ b/packages/slice-machine/src/features/onboarding/useOnboarding.ts @@ -0,0 +1,62 @@ +import { OnboardingState } from "@prismicio/editor-fields"; +import { useRefGetter } from "@prismicio/editor-support/React"; +import { updateData, useRequest } from "@prismicio/editor-support/Suspense"; +import { toast } from "react-toastify"; + +import { managerClient } from "@/managerClient"; + +const { fetchOnboarding, toggleOnboarding, toggleOnboardingStep } = + managerClient.prismicRepository; + +async function getOnboarding() { + try { + return fetchOnboarding(); + } catch (error) { + console.error("Failed to fetch onboarding", error); + return undefined; + } +} + +export function useOnboarding() { + const onboarding = useRequest(getOnboarding, []); + const getOnboardingState = useRefGetter(onboarding); + + function updateCache(newOnboardingState: OnboardingState) { + updateData(getOnboarding, [], newOnboardingState); + } + + async function toggleStep(stepId: string) { + const onboardingState = getOnboardingState(); + if (!onboardingState) return []; + + try { + const { completedSteps } = await toggleOnboardingStep(stepId); + updateCache({ ...onboardingState, completedSteps }); + + return completedSteps; + } catch (error) { + toast.error("Failed to complete/undo step"); + console.error("Error toggling onboarding step", error); + + return onboardingState.completedSteps; + } + } + + async function toggleGuide() { + const onboardingState = getOnboardingState(); + if (!onboardingState) return; + + const wasDismissed = onboardingState.isDismissed; + try { + updateCache({ ...onboardingState, isDismissed: !wasDismissed }); // optimistic + const { isDismissed } = await toggleOnboarding(); + updateCache({ ...onboardingState, isDismissed }); // sync with api + } catch (error) { + updateCache({ ...onboardingState, isDismissed: wasDismissed }); // rollback + toast.error("Failed to hide/show onboarding"); + console.error("Error toggling onboarding", error); + } + } + + return { onboarding, toggleStep, toggleGuide }; +} diff --git a/packages/slice-machine/src/features/onboarding/useSharedOnboardingExperiment.ts b/packages/slice-machine/src/features/onboarding/useSharedOnboardingExperiment.ts new file mode 100644 index 0000000000..399d6ee967 --- /dev/null +++ b/packages/slice-machine/src/features/onboarding/useSharedOnboardingExperiment.ts @@ -0,0 +1,6 @@ +import { useExperimentVariant } from "@/hooks/useExperimentVariant"; + +export const useSharedOnboardingExperiment = () => { + const variant = useExperimentVariant("slicemachine-shared-onboarding"); + return { eligible: variant?.value === "with-shared-onboarding" }; +}; diff --git a/packages/slice-machine/src/legacy/components/Navigation/index.tsx b/packages/slice-machine/src/legacy/components/Navigation/index.tsx index f89c10dec1..bd54a010fe 100644 --- a/packages/slice-machine/src/legacy/components/Navigation/index.tsx +++ b/packages/slice-machine/src/legacy/components/Navigation/index.tsx @@ -16,7 +16,7 @@ import { ErrorBoundary } from "@/ErrorBoundary"; import { CUSTOM_TYPES_CONFIG } from "@/features/customTypes/customTypesConfig"; import { CUSTOM_TYPES_MESSAGES } from "@/features/customTypes/customTypesMessages"; import { MasterSliceLibraryPreviewModal } from "@/features/masterSliceLibrary/SliceLibraryPreviewModal"; -import { OnboardingGuide } from "@/features/onboarding/OnboardingGuide"; +import { OnboardingGuide } from "@/features/onboarding"; import { useGitIntegrationExperiment } from "@/features/settings/git/useGitIntegrationExperiment"; import { useMarketingContent } from "@/hooks/useMarketingContent"; import { useRepositoryInformation } from "@/hooks/useRepositoryInformation"; diff --git a/packages/slice-machine/test/src/modules/__fixtures__/serverState.ts b/packages/slice-machine/test/src/modules/__fixtures__/serverState.ts index 594410c635..b1bb251508 100644 --- a/packages/slice-machine/test/src/modules/__fixtures__/serverState.ts +++ b/packages/slice-machine/test/src/modules/__fixtures__/serverState.ts @@ -24,6 +24,7 @@ export const dummyServerState: Pick< PrismicEmbed: "https://oembed.prismic.io", PrismicUnsplash: "https://unsplash.prismic.io/", SliceMachineV1: "https://sm-api.prismic.io/v1/", + RepositoryService: "https://repository.prismic.io/", }, shortId: "shortId", }, diff --git a/yarn.lock b/yarn.lock index 3572ca472d..7533dc8814 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5981,12 +5981,12 @@ __metadata: languageName: node linkType: hard -"@prismicio/editor-fields@npm:0.4.49": - version: 0.4.49 - resolution: "@prismicio/editor-fields@npm:0.4.49" +"@prismicio/editor-fields@npm:0.4.51": + version: 0.4.51 + resolution: "@prismicio/editor-fields@npm:0.4.51" dependencies: "@floating-ui/react-dom-interactions": 0.9.3 - "@prismicio/editor-support": 0.4.49 + "@prismicio/editor-support": 0.4.51 "@prismicio/richtext": 2.1.1 "@prismicio/types-internal": 2.8.0 "@tanstack/react-query": 5.55.4 @@ -6017,16 +6017,16 @@ __metadata: tslib: 2.4.0 zod: 3.21.4 peerDependencies: - "@prismicio/editor-ui": ^0.4.49 + "@prismicio/editor-ui": ^0.4.51 react: 18 react-dom: 18 - checksum: afea768d653320daac10d96b1fb24c2b52681872672d3603d28b8454a1e374c9743fa4fb2c3613cd87c5e66ba8c2ec5813d5e31910cbf193f3d2d4d4b11b28f8 + checksum: a416a600b3576dd0448ac1b863b2509914997796900f759dd8f4af86c7d46857ac0a2110e7a07664a98d0f968b71ac9f4d110cc35e0dd5851349078b5cde5e82 languageName: node linkType: hard -"@prismicio/editor-support@npm:0.4.49": - version: 0.4.49 - resolution: "@prismicio/editor-support@npm:0.4.49" +"@prismicio/editor-support@npm:0.4.51": + version: 0.4.51 + resolution: "@prismicio/editor-support@npm:0.4.51" dependencies: tslib: 2.4.0 peerDependencies: @@ -6037,16 +6037,16 @@ __metadata: optional: true zod: optional: true - checksum: a3e65155b71377ebaa274302d3f16af481b1e2c8c4b8be0a4144cc22ec12cd3d8e32e98d9cfa0d3bd1444b4f6887d6e08779484a1fac49361e17c46a2beb438d + checksum: 12a86ee80b180c9efe08795ad704e9a8012f20ccebf93fd9a73e1abdf59fd9f61266caf3ff4eec452a8a81ba1d92ca8c7d77c5093282dd4a28458257cd81affa languageName: node linkType: hard -"@prismicio/editor-ui@npm:0.4.49": - version: 0.4.49 - resolution: "@prismicio/editor-ui@npm:0.4.49" +"@prismicio/editor-ui@npm:0.4.51": + version: 0.4.51 + resolution: "@prismicio/editor-ui@npm:0.4.51" dependencies: "@internationalized/date": 3.5.5 - "@prismicio/editor-support": 0.4.49 + "@prismicio/editor-support": 0.4.51 "@radix-ui/react-avatar": 1.1.0 "@radix-ui/react-checkbox": 1.1.1 "@radix-ui/react-dialog": 1.1.1 @@ -6097,7 +6097,7 @@ __metadata: peerDependencies: react: 17 || 18 react-dom: 17 || 18 - checksum: 264cc81b675cd8785b6a088c173d9c9f6ed4cb87857ed276fedcc3d820099f66967e0d388b81c113c361b2f4f4f95024e33118d711424c2c748b28fa682394f9 + checksum: 53fa9ebfa291e5b23887345ca2b25bab676e2bb681a002a70df2439d808216576db1c4a7817b8802142449991d3c150c485a4edda9aea8fdeb4466beea6c9767 languageName: node linkType: hard @@ -17179,13 +17179,6 @@ __metadata: languageName: node linkType: hard -"dom-confetti@npm:0.2.2": - version: 0.2.2 - resolution: "dom-confetti@npm:0.2.2" - checksum: 6b14f4432fa758b038d63cfb7c8d56577d4ce99b11b7a7c74d261e5fb2fb4e2495c6b6787ba78c778cfad2627730612234424f675c524d903ad8d7485319af22 - languageName: node - linkType: hard - "dom-converter@npm:^0.2.0": version: 0.2.0 resolution: "dom-converter@npm:0.2.0" @@ -31206,9 +31199,9 @@ __metadata: "@emotion/react": 11.11.1 "@extractus/oembed-extractor": 3.1.8 "@prismicio/client": 7.11.0 - "@prismicio/editor-fields": 0.4.49 - "@prismicio/editor-support": 0.4.49 - "@prismicio/editor-ui": 0.4.49 + "@prismicio/editor-fields": 0.4.51 + "@prismicio/editor-support": 0.4.51 + "@prismicio/editor-ui": 0.4.51 "@prismicio/mock": 0.3.3 "@prismicio/mocks": 2.4.0 "@prismicio/simulator": 0.1.4 @@ -31242,7 +31235,6 @@ __metadata: connected-next-router: 4.2.0 cross-env: 7.0.3 depcheck: 1.4.3 - dom-confetti: 0.2.2 eslint: 8.56.0 eslint-config-prettier: 9.0.0 eslint-import-resolver-typescript: 3.5.3