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