Skip to content

Commit

Permalink
[DT-2388] Complete onboarding steps with user page and slice actions (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jomifepe authored Oct 28, 2024
1 parent 1eef1fd commit c00a5b7
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 35 deletions.
15 changes: 15 additions & 0 deletions packages/init/src/SliceMachineInitProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,21 @@ ${chalk.cyan("?")} Your Prismic repository name`.replace("\n", ""),
framework: this.context.framework.wroomTelemetryID,
starterId: this.context.starterId,
});

try {
const { value: onboardingExperimentVariant } =
(await this.manager.telemetry.getExperimentVariant(
"shared-onboarding",
)) ?? {};
if (onboardingExperimentVariant === "with-shared-onboarding") {
this.manager.prismicRepository.completeOnboardingStep(
"createProject",
"setupSliceMachine",
);
}
} catch (error) {
await this.trackError(error);
}
} catch (error) {
// When we have an error here, it's most probably because the user has a stale SESSION cookie

Expand Down
4 changes: 2 additions & 2 deletions packages/manager/src/constants/API_ENDPOINTS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const API_ENDPOINTS: APIEndpoints = (() => {
"https://mc5qopc07a.execute-api.us-east-1.amazonaws.com/v1/",
),
RepositoryService: addTrailingSlash(
process.env.repository_api ?? "https://repository.wroom.io/",
process.env.repository_api ?? "https://repository.internal.wroom.io/",
),
};

Expand Down Expand Up @@ -85,7 +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/",
RepositoryService: "https://repository.internal.wroom.io/",
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,62 @@ export class PrismicRepositoryManager extends BaseManager {
}
}

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

const currentState = await this.fetchOnboarding();
const incompleteSteps = stepIds.filter(
(stepId) => !currentState.completedSteps.includes(stepId),
);

if (incompleteSteps.length > 0) {
// TODO: Refactor when the API accepts multiple steps (DT-2389)
for await (const stepId of incompleteSteps) {
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 complete response: ${error.errors.join(
", ",
)}`,
);
}

if (value) {
currentState.completedSteps = value.completedSteps;
continue;
}
}

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

return { completedSteps: currentState.completedSteps };
}

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

Expand Down
6 changes: 3 additions & 3 deletions packages/slice-machine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.51",
"@prismicio/editor-support": "0.4.51",
"@prismicio/editor-ui": "0.4.51",
"@prismicio/editor-fields": "0.4.54",
"@prismicio/editor-support": "0.4.54",
"@prismicio/editor-ui": "0.4.54",
"@prismicio/mock": "0.3.3",
"@prismicio/mocks": "2.4.0",
"@prismicio/simulator": "0.1.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ContentTabs } from "@/components/ContentTabs";
import { ErrorBoundary } from "@/ErrorBoundary";
import { MarkdownRenderer } from "@/features/documentation/MarkdownRenderer";
import { useDocumentation } from "@/features/documentation/useDocumentation";
import { useOnboarding } from "@/features/onboarding/useOnboarding";
import { useAdapterName } from "@/hooks/useAdapterName";

import styles from "./PageSnippetDialog.module.css";
Expand All @@ -19,12 +20,14 @@ const PageSnippetContent: FC<PageSnippetContentProps> = ({ model }) => {
kind: "PageSnippet",
data: { model },
});
const { completeStep } = useOnboarding();

if (documentation.length === 0) {
return null;
}

const trackOpenSnippet = () => {
void completeStep("codePage");
void telemetry.track({
event: "page-type:open-snippet",
framework: adapter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
Text,
useMediaQuery,
} from "@prismicio/editor-ui";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";

import styles from "./OnboardingGuide.module.css";
import { OnboardingProgressStepper } from "./OnboardingProgressStepper";
Expand All @@ -22,7 +22,7 @@ export function SliceMachineOnboardingGuide() {
return (
<OnboardingProvider onComplete={confetti.throwConfetti}>
<div ref={confetti.confettiContainerRef} className={styles.container}>
<OnboardingGuideCard />
<OnboardingGuideCard setVisible={setVisible} />
<div
ref={confetti.confettiCannonRef}
className={styles.confettiCannon}
Expand All @@ -32,10 +32,24 @@ export function SliceMachineOnboardingGuide() {
);
}

function OnboardingGuideCard() {
type OnboardingGuideCardProps = {
setVisible: (isVisible: boolean) => void;
};

function OnboardingGuideCard(props: OnboardingGuideCardProps) {
const { setVisible } = props;
const { steps, completedStepCount, isComplete } = useOnboardingContext();
const isVisible = useMediaQuery({ min: "medium" });

const isMountedRef = useRef(false);
useEffect(() => {
// quick fix to prevent having the onboarding invisible but interactive when complete
if (!isMountedRef.current) {
isMountedRef.current = true;
setVisible(false);
}
}, [isVisible, setVisible]);

if (!isVisible) return null;

return (
Expand Down
44 changes: 36 additions & 8 deletions packages/slice-machine/src/features/onboarding/useOnboarding.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
import { OnboardingState } from "@prismicio/editor-fields";
import { OnboardingState, OnboardingStepId } 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 { useSharedOnboardingExperiment } from "@/features/onboarding/useSharedOnboardingExperiment";
import { managerClient } from "@/managerClient";

const { fetchOnboarding, toggleOnboarding, toggleOnboardingStep } =
managerClient.prismicRepository;

async function getOnboarding() {
try {
return fetchOnboarding();
return await fetchOnboarding();
} catch (error) {
console.error("Failed to fetch onboarding", error);
return undefined;
}
}

const noop = () => Promise.resolve(undefined);

export function useOnboarding() {
const onboarding = useRequest(getOnboarding, []);
const isSharedExperimentEligible = useSharedOnboardingExperiment().eligible;
const onboarding = useRequest(
isSharedExperimentEligible ? getOnboarding : noop,
[],
);
const getOnboardingState = useRefGetter(onboarding);

function updateCache(newOnboardingState: OnboardingState) {
updateData(getOnboarding, [], newOnboardingState);
}

async function toggleStep(stepId: string) {
async function toggleStep(stepId: OnboardingStepId) {
if (!isSharedExperimentEligible) return [];

const onboardingState = getOnboardingState();
if (!onboardingState) return [];

try {
const { completedSteps } = await toggleOnboardingStep(stepId);
const { completedSteps } = await toggleOnboardingStep(String(stepId));
updateCache({ ...onboardingState, completedSteps });

return completedSteps;
} catch (error) {
toast.error("Failed to complete/undo step");
console.error("Error toggling onboarding step", error);
console.error("Failed to toggle onboarding step", error);

return onboardingState.completedSteps;
}
}

async function toggleGuide() {
if (!isSharedExperimentEligible) return;

const onboardingState = getOnboardingState();
if (!onboardingState) return;

Expand All @@ -54,9 +65,26 @@ export function useOnboarding() {
} catch (error) {
updateCache({ ...onboardingState, isDismissed: wasDismissed }); // rollback
toast.error("Failed to hide/show onboarding");
console.error("Error toggling onboarding", error);
console.error("Failed to toggle onboarding", error);
}
}

async function completeStep(stepId: OnboardingStepId) {
if (!isSharedExperimentEligible) return;

const onboardingState = getOnboardingState();
if (!onboardingState) return;

try {
// TODO: Refactor when the API has complete action (DT-2389)
if (!onboardingState.completedSteps.includes(String(stepId))) {
await toggleStep(stepId);
}
} catch (error) {
toast.error("Failed to complete onboarding step");
console.error("Failed to complete onboarding step", error);
}
}

return { onboarding, toggleStep, toggleGuide };
return { onboarding, toggleStep, completeStep, toggleGuide };
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useExperimentVariant } from "@/hooks/useExperimentVariant";

export const useSharedOnboardingExperiment = () => {
const variant = useExperimentVariant("slicemachine-shared-onboarding");
const variant = useExperimentVariant("shared-onboarding");
return { eligible: variant?.value === "with-shared-onboarding" };
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@/features/customTypes/actions/createCustomType";
import { CUSTOM_TYPES_CONFIG } from "@/features/customTypes/customTypesConfig";
import { CUSTOM_TYPES_MESSAGES } from "@/features/customTypes/customTypesMessages";
import { useOnboarding } from "@/features/onboarding/useOnboarding";
import { useAutoSync } from "@/features/sync/AutoSyncProvider";
import ModalFormCard from "@/legacy/components/ModalFormCard";
import { API_ID_REGEX } from "@/legacy/lib/consts";
Expand Down Expand Up @@ -50,6 +51,7 @@ export const CreateCustomTypeModal: React.FC<CreateCustomTypeModalProps> = ({
onOpenChange,
}) => {
const { createCustomTypeSuccess } = useSliceMachineActions();
const { completeStep } = useOnboarding();

const { customTypeIds, customTypeLabels } = useSelector(
(store: SliceMachineStoreType) => ({
Expand Down Expand Up @@ -84,6 +86,8 @@ export const CreateCustomTypeModal: React.FC<CreateCustomTypeModalProps> = ({
});

syncChanges();

if (format === "page") void completeStep("createPageType");
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Select from "react-select";
import { Box, Label } from "theme-ui";

import { getState } from "@/apiClient";
import { useOnboarding } from "@/features/onboarding/useOnboarding";
import { createSlice } from "@/features/slices/actions/createSlice";
import { useAutoSync } from "@/features/sync/AutoSyncProvider";
import ModalFormCard from "@/legacy/components/ModalFormCard";
Expand Down Expand Up @@ -32,6 +33,7 @@ export const CreateSliceModal: FC<CreateSliceModalProps> = ({
const { createSliceSuccess } = useSliceMachineActions();
const [isCreatingSlice, setIsCreatingSlice] = useState(false);
const { syncChanges } = useAutoSync();
const { completeStep } = useOnboarding();

const onSubmit = async (values: FormValues) => {
const sliceName = values.sliceName;
Expand All @@ -49,6 +51,7 @@ export const CreateSliceModal: FC<CreateSliceModalProps> = ({
createSliceSuccess(serverState.libraries);
onSuccess(newSlice, libraryName);
syncChanges();
void completeStep("createSlice");
},
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { telemetry } from "@/apiClient";
import { ListHeader } from "@/components/List";
import { useCustomTypeState } from "@/features/customTypes/customTypesBuilder/CustomTypeProvider";
import { SliceZoneBlankSlate } from "@/features/customTypes/customTypesBuilder/SliceZoneBlankSlate";
import { useOnboarding } from "@/features/onboarding/useOnboarding";
import { addSlicesToSliceZone } from "@/features/slices/actions/addSlicesToSliceZone";
import { useSlicesTemplates } from "@/features/slicesTemplates/useSlicesTemplates";
import { CreateSliceModal } from "@/legacy/components/Forms/CreateSliceModal";
Expand Down Expand Up @@ -124,6 +125,7 @@ const SliceZone: React.FC<SliceZoneProps> = ({
}),
);
const { setCustomType } = useCustomTypeState();
const { completeStep } = useOnboarding();

const localLibraries: readonly LibraryUI[] = libraries.filter(
(library) => library.isLocal,
Expand Down Expand Up @@ -299,6 +301,7 @@ const SliceZone: React.FC<SliceZoneProps> = ({
setCustomType(CustomTypes.fromSM(newCustomType), () => {
toast.success("Slice(s) added to slice zone");
});
void completeStep("createSlice");
closeUpdateSliceZoneModal();
}}
close={closeUpdateSliceZoneModal}
Expand All @@ -323,6 +326,7 @@ const SliceZone: React.FC<SliceZoneProps> = ({
/>,
);
});
void completeStep("createSlice");
closeSlicesTemplatesModal();
}}
close={closeSlicesTemplatesModal}
Expand Down
4 changes: 4 additions & 0 deletions packages/slice-machine/src/pages/changes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getState, telemetry } from "@/apiClient";
import { BreadcrumbItem } from "@/components/Breadcrumb";
import { NoChangesBlankSlate } from "@/features/changes/BlankSlates";
import { PushChangesButton } from "@/features/changes/PushChangesButton";
import { useOnboarding } from "@/features/onboarding/useOnboarding";
import { pushChanges } from "@/features/sync/actions/pushChanges";
import { useAutoSync } from "@/features/sync/AutoSyncProvider";
import { useUnSyncChanges } from "@/features/sync/useUnSyncChanges";
Expand Down Expand Up @@ -56,6 +57,7 @@ const Changes: React.FunctionComponent = () => {
const [isPushed, setIsPushed] = useState(false);
const [isToastOpen, setIsToastOpen] = useState(false);
const { repositoryName } = useRepositoryInformation();
const { completeStep } = useOnboarding();

const documentsListEndpoint =
createDocumentsListEndpointFromRepoName(repositoryName);
Expand Down Expand Up @@ -95,6 +97,8 @@ const Changes: React.FunctionComponent = () => {

setIsPushed(true);
setIsToastOpen(true);

void completeStep("reviewAndPush");
}
} catch (error) {
console.error(
Expand Down
Loading

0 comments on commit c00a5b7

Please sign in to comment.