Path: blob/main/components/dashboard/src/onboarding/UserOnboarding.tsx
2500 views
/**1* Copyright (c) 2023 Gitpod GmbH. All rights reserved.2* Licensed under the GNU Affero General Public License (AGPL).3* See License.AGPL.txt in the project root for license information.4*/56import { User } from "@gitpod/public-api/lib/gitpod/v1/user_pb";7import { FunctionComponent, useCallback, useContext, useState } from "react";8import gitpodIcon from "../icons/gitpod.svg";9import { Separator } from "../components/Separator";10import { useHistory, useLocation } from "react-router";11import { StepUserInfo } from "./StepUserInfo";12import { UserContext } from "../user-context";13import { StepOrgInfo } from "./StepOrgInfo";14import { StepPersonalize } from "./StepPersonalize";15import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutation";16import Alert from "../components/Alert";17import { useConfetti } from "../contexts/ConfettiContext";18import { trackEvent } from "../Analytics";1920// This param is optionally present to force an onboarding flow21// Can be used if other conditions aren't true, i.e. if user has already onboarded, but we want to force the flow again22export const FORCE_ONBOARDING_PARAM = "onboarding";23export const FORCE_ONBOARDING_PARAM_VALUE = "force";2425const STEPS = {26ONE: "one",27TWO: "two",28THREE: "three",29};30type Props = {31user: User;32};33const UserOnboarding: FunctionComponent<Props> = ({ user }) => {34const history = useHistory();35const location = useLocation();36const { setUser } = useContext(UserContext);37const updateUser = useUpdateCurrentUserMutation();38const { dropConfetti } = useConfetti();3940const [step, setStep] = useState(STEPS.ONE);41const [completingError, setCompletingError] = useState("");4243// We track this state here so we can persist it at the end of the flow instead of when it's selected44// This is because setting the ide is how we indicate a user has onboarded, and want to defer that until the end45// even though we may ask for it earlier in the flow. The tradeoff is a potential error state at the end of the flow when updating the IDE46const [ideOptions, setIDEOptions] = useState({ ide: "code", useLatest: false });4748// TODO: This logic can be simplified in the future if we put existing users through onboarding and track the onboarded timestamp49// When onboarding is complete (last step finished), we do the following50// * Update the user's IDE selection (current logic relies on this for considering a user onboarded, so we wait until the end)51// * Set an user's onboarded timestamp52// * Update the `user` context w/ the latest user, which will close out this onboarding flow53const onboardingComplete = useCallback(54async (updatedUser: User) => {55try {56const updates = {57additionalData: {58profile: {59onboardedTimestamp: new Date().toISOString(),60},61ideSettings: {62settingVersion: "2.0",63defaultIde: ideOptions.ide,64useLatestVersion: ideOptions.useLatest,65},66},67};6869try {70// TODO: extract the IDE updating into it's own step, and add a mutation for it once we don't rely on it to consider a user being "onboarded"71// We can do this once we rely on the profile.onboardedTimestamp instead.72const onboardedUser = await updateUser.mutateAsync(updates);7374// TODO: move this into a mutation side effect once we have a specific mutation for updating the IDE (see above TODO)75trackEvent("ide_configuration_changed", {76name: onboardedUser.editorSettings?.name,77version: onboardedUser.editorSettings?.version,78location: "onboarding",79});8081dropConfetti();82setUser(onboardedUser);8384// Look for the `onboarding=force` query param, and remove if present85const queryParams = new URLSearchParams(location.search);86if (queryParams.get(FORCE_ONBOARDING_PARAM) === FORCE_ONBOARDING_PARAM_VALUE) {87queryParams.delete(FORCE_ONBOARDING_PARAM);88history.replace({89pathname: location.pathname,90search: queryParams.toString(),91hash: location.hash,92});93}94} catch (e) {95console.log("error caught", e);96console.error(e);97setCompletingError("There was a problem completing your onboarding");98}99} catch (e) {100console.error(e);101}102},103[104history,105ideOptions.ide,106ideOptions.useLatest,107location.hash,108location.pathname,109location.search,110setUser,111dropConfetti,112updateUser,113],114);115116return (117<div className="container">118<div className="app-container">119<div className="flex items-center justify-center py-3">120<img src={gitpodIcon} className="h-6" alt="Gitpod's logo" />121</div>122<Separator />123<div className="mt-24">124{step === STEPS.ONE && (125<StepUserInfo126user={user}127onComplete={(updatedUser) => {128setUser(updatedUser);129setStep(STEPS.TWO);130}}131/>132)}133{step === STEPS.TWO && (134<StepPersonalize135user={user}136onComplete={(ide, useLatest) => {137setIDEOptions({ ide, useLatest });138setStep(STEPS.THREE);139}}140/>141)}142{step === STEPS.THREE && <StepOrgInfo user={user} onComplete={onboardingComplete} />}143144{!!completingError && <Alert type="error">{completingError}</Alert>}145</div>146</div>147</div>148);149};150export default UserOnboarding;151152153