Skip to content

Commit 6293167

Browse files
committed
Implement new user onboarding flow with welcome message
Tool: gitpod/catfood.gitpod.cloud
1 parent 0d7d880 commit 6293167

File tree

10 files changed

+73
-45
lines changed

10 files changed

+73
-45
lines changed

components/dashboard/public/complete-auth/index.html

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,31 @@
66
-->
77

88
<html>
9+
910
<head>
1011
<meta charset='utf-8'>
1112
<title>Done</title>
1213
<script>
1314
if (window.opener) {
1415
const search = new URLSearchParams(window.location.search);
1516
const message = search.get("message");
16-
window.opener.postMessage(message, `https://${window.location.hostname}`);
17+
const newUser = search.get("newUser");
18+
window.opener.postMessage(message + "&newUser=" + newUser, `https://${window.location.hostname}`);
19+
20+
if (newUser === "true") {
21+
localStorage.setItem("newUserOnboardingPending", newUser);
22+
}
1723
} else {
1824
console.log("This page is supposed to be opened by Gitpod.")
1925
setTimeout("window.close();", 1000);
2026
}
2127
</script>
2228
</head>
29+
2330
<body>
24-
If this tab is not closed automatically, feel free to close it and proceed. <button className="px-4 py-2 my-auto bg-gray-900 hover:bg-gray-800 dark:bg-kumquat-base dark:hover:bg-kumquat-ripe text-gray-50 dark:text-gray-900 text-sm font-medium rounded-xl focus:outline-none focus:ring transition ease-in-out" type="button" onclick="window.open('', '_self', ''); window.close();">Close</button>
31+
If this tab is not closed automatically, feel free to close it and proceed. <button
32+
className="px-4 py-2 my-auto bg-gray-900 hover:bg-gray-800 dark:bg-kumquat-base dark:hover:bg-kumquat-ripe text-gray-50 dark:text-gray-900 text-sm font-medium rounded-xl focus:outline-none focus:ring transition ease-in-out"
33+
type="button" onclick="window.open('', '_self', ''); window.close();">Close</button>
2534
</body>
35+
2636
</html>

components/dashboard/src/login/SSOLoginForm.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ import { useOnboardingState } from "../dedicated-setup/use-needs-setup";
1616
import { getOrgSlugFromQuery } from "../data/organizations/orgs-query";
1717
import { storageAvailable } from "../utils";
1818

19-
type Props = {
20-
onSuccess: () => void;
21-
};
22-
2319
function getOrgSlugFromPath(path: string) {
2420
// '/login/acme' => ['', 'login', 'acme']
2521
const pathSegments = path.split("/");
@@ -29,6 +25,9 @@ function getOrgSlugFromPath(path: string) {
2925
return pathSegments[2];
3026
}
3127

28+
type Props = {
29+
onSuccess: () => void;
30+
};
3231
export const SSOLoginForm: FC<Props> = ({ onSuccess }) => {
3332
const location = useLocation();
3433
const { data: onboardingState } = useOnboardingState();

components/dashboard/src/start/StartWorkspace.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ function parseParameters(search?: string): { notFound?: boolean } {
8888

8989
export interface StartWorkspaceState {
9090
/**
91-
* This is set to the istanceId we started (think we started on).
91+
* This is set to the instanceId we started (think we started on).
9292
* We only receive updates for this particular instance, or none if not set.
9393
*/
9494
startedInstanceId?: string;

components/dashboard/src/teams/TeamOnboarding.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ export default function TeamOnboardingPage() {
166166
Here's a preview of the welcome message that will be shown to your organization members:
167167
</span>
168168
<WelcomeMessagePreview
169-
welcomeMessage={settings?.onboardingSettings?.welcomeMessage?.message}
170169
setWelcomeMessageEditorOpen={setWelcomeMessageEditorOpen}
171170
disabled={!isOwner || updateTeamSettings.isLoading}
172171
/>
@@ -177,25 +176,23 @@ export default function TeamOnboardingPage() {
177176
}
178177

179178
type WelcomeMessagePreviewProps = {
180-
welcomeMessage: string | undefined;
181179
disabled?: boolean;
182-
setWelcomeMessageEditorOpen: (open: boolean) => void;
180+
setWelcomeMessageEditorOpen?: (open: boolean) => void;
183181
};
184-
const WelcomeMessagePreview = ({
185-
welcomeMessage,
186-
disabled,
187-
setWelcomeMessageEditorOpen,
188-
}: WelcomeMessagePreviewProps) => {
182+
export const WelcomeMessagePreview = ({ disabled, setWelcomeMessageEditorOpen }: WelcomeMessagePreviewProps) => {
189183
const { data: settings } = useOrgSettingsQuery();
190184
const avatarUrl = settings?.onboardingSettings?.welcomeMessage?.featuredMemberResolvedAvatarUrl;
185+
const welcomeMessage = settings?.onboardingSettings?.welcomeMessage?.message;
191186

192187
return (
193188
<div className="max-w-2xl mx-auto">
194189
<div className="flex justify-between gap-2 items-center">
195190
<Heading2 className="py-6">Welcome to Gitpod</Heading2>
196-
<Button variant="secondary" onClick={() => setWelcomeMessageEditorOpen(true)} disabled={disabled}>
197-
Edit
198-
</Button>
191+
{setWelcomeMessageEditorOpen && (
192+
<Button variant="secondary" onClick={() => setWelcomeMessageEditorOpen(true)} disabled={disabled}>
193+
Edit
194+
</Button>
195+
)}
199196
</div>
200197
<Subheading>{gitpodWelcomeSubheading}</Subheading>
201198
{/* todo: sanitize md */}

components/dashboard/src/workspaces/Workspaces.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import { useUpdateCurrentUserMutation } from "../data/current-user/update-mutati
3737
import { useUserLoader } from "../hooks/use-user-loader";
3838
import Tooltip from "../components/Tooltip";
3939
import { useFeatureFlag } from "../data/featureflag-query";
40+
import { storageAvailable } from "../utils";
41+
import { WelcomeMessagePreview } from "../teams/TeamOnboarding";
4042

4143
export const GETTING_STARTED_DISMISSAL_KEY = "workspace-list-getting-started";
4244

@@ -46,6 +48,18 @@ const WorkspacesPage: FunctionComponent = () => {
4648
const [showInactive, setShowInactive] = useState(false);
4749
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
4850

51+
const orgOnboardingPending = useMemo(() => {
52+
if (storageAvailable("localStorage")) {
53+
return localStorage.getItem("newUserOnboardingPending") === "true";
54+
}
55+
return false;
56+
}, []);
57+
const dismissOrgOnboardingPending = useCallback(() => {
58+
if (storageAvailable("localStorage")) {
59+
localStorage.removeItem("newUserOnboardingPending");
60+
}
61+
}, []);
62+
4963
const { data, isLoading } = useListWorkspacesQuery({ limit });
5064
const deleteInactiveWorkspaces = useDeleteInactiveWorkspacesMutation();
5165
useListenToWorkspacesStatusUpdates();
@@ -400,6 +414,12 @@ const WorkspacesPage: FunctionComponent = () => {
400414
) : (
401415
<EmptyWorkspacesContent />
402416
))}
417+
418+
<Modal visible={orgOnboardingPending} onClose={dismissOrgOnboardingPending}>
419+
<ModalBody>
420+
<WelcomeMessagePreview />
421+
</ModalBody>
422+
</Modal>
403423
</>
404424
);
405425
};

components/public-api-server/pkg/oidc/router.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,16 @@ func (s *Service) getCallbackHandler() http.HandlerFunc {
169169
return
170170
}
171171
} else {
172-
cookies, _, err := s.createSession(r.Context(), result, config)
172+
cookies, message, err := s.createSession(r.Context(), result, config)
173173
if err != nil {
174174
log.WithError(err).Warn("Failed to create session from downstream session provider.")
175175
reportLoginCompleted("failed", "sso")
176176
respondeWithError(rw, r, "We were unable to create a user session.", http.StatusInternalServerError, useHttpErrors)
177177
return
178178
}
179+
if message.NewUser {
180+
oauth2Result.ReturnToURL += "&newUser=true"
181+
}
179182
for _, cookie := range cookies {
180183
http.SetCookie(rw, cookie)
181184
}

components/public-api-server/pkg/oidc/service.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,12 @@ func (s *Service) authenticate(ctx context.Context, params authenticateParams) (
323323
}, nil
324324
}
325325

326-
func (s *Service) createSession(ctx context.Context, flowResult *AuthFlowResult, clientConfig *ClientConfig) ([]*http.Cookie, string, error) {
326+
type Message struct {
327+
NewUser bool `json:"newUser"`
328+
UserID string `json:"userId"`
329+
}
330+
331+
func (s *Service) createSession(ctx context.Context, flowResult *AuthFlowResult, clientConfig *ClientConfig) ([]*http.Cookie, Message, error) {
327332
type CreateSessionPayload struct {
328333
AuthFlowResult
329334
OrganizationID string `json:"organizationId"`
@@ -336,26 +341,30 @@ func (s *Service) createSession(ctx context.Context, flowResult *AuthFlowResult,
336341
}
337342
payload, err := json.Marshal(sessionPayload)
338343
if err != nil {
339-
return nil, "", err
344+
return nil, Message{}, fmt.Errorf("failed to construct session request: %w", err)
340345
}
341346

342347
url := fmt.Sprintf("http://%s/session", s.sessionServiceAddress)
343348
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(payload))
344349
if err != nil {
345-
return nil, "", fmt.Errorf("failed to construct session request: %w", err)
350+
return nil, Message{}, fmt.Errorf("failed to construct session request: %w", err)
346351
}
347352
req.Header.Set("Content-Type", "application/json")
348353

349354
res, err := http.DefaultClient.Do(req)
350355
if err != nil {
351-
return nil, "", fmt.Errorf("failed to make request to /session endpoint: %w", err)
356+
return nil, Message{}, fmt.Errorf("failed to make request to /session endpoint: %w", err)
352357
}
353358

354359
body, err := io.ReadAll(res.Body)
355360
if err != nil {
356-
return nil, "", err
361+
return nil, Message{}, err
362+
}
363+
message := Message{}
364+
err = json.Unmarshal(body, &message)
365+
if err != nil {
366+
return nil, Message{}, err
357367
}
358-
message := string(body)
359368

360369
if res.StatusCode == http.StatusOK {
361370
return res.Cookies(), message, nil

components/public-api-server/pkg/oidc/service_test.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"context"
99
"encoding/json"
1010
"fmt"
11-
"io"
1211
"log"
1312
"net/http"
1413
"net/http/httptest"
@@ -25,7 +24,6 @@ import (
2524
"github.com/go-chi/chi/v5"
2625
"github.com/go-chi/chi/v5/middleware"
2726
"github.com/golang-jwt/jwt/v5"
28-
"github.com/google/go-cmp/cmp"
2927
"github.com/google/uuid"
3028
"github.com/stretchr/testify/require"
3129
"golang.org/x/oauth2"
@@ -277,20 +275,8 @@ func TestCreateSession(t *testing.T) {
277275
_, message, err := service.createSession(context.Background(), &AuthFlowResult{}, &config)
278276
require.NoError(t, err, "failed to create session")
279277

280-
got := map[string]interface{}{}
281-
err = json.Unmarshal([]byte(message), &got)
282-
require.NoError(t, err, "failed to parse response")
283-
284-
expected := map[string]interface{}{
285-
"claims": nil,
286-
"idToken": nil,
287-
"oidcClientConfigId": config.ID,
288-
"organizationId": config.OrganizationID,
289-
}
290-
291-
if diff := cmp.Diff(expected, got); diff != "" {
292-
t.Errorf("Unexpected create session payload (-want +got):\n%s", diff)
293-
}
278+
require.True(t, message.NewUser, "expected new user")
279+
require.Equal(t, message.UserID, "user-id", "expected user id")
294280
}
295281

296282
func Test_validateRequiredClaims(t *testing.T) {
@@ -537,11 +523,13 @@ func newFakeSessionServer(t *testing.T) string {
537523
})
538524
w.WriteHeader(http.StatusOK)
539525

540-
// mirroring back the request body for testing
541-
body, err := io.ReadAll(r.Body)
542-
if err != nil {
543-
body = []byte(err.Error())
526+
message := Message{
527+
NewUser: true,
528+
UserID: "user-id",
544529
}
530+
body, err := json.Marshal(message)
531+
require.NoError(t, err)
532+
545533
_, err = w.Write(body)
546534
if err != nil {
547535
log.Fatal(err)

components/server/src/auth/login-completion-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class LoginCompletionHandler {
3939
) {
4040
const logContext = LogContext.from({ user, request });
4141

42+
log.info("Login completion handler", { user, returnToUrl, authHost, elevateScopes });
4243
try {
4344
await new Promise<void>((resolve, reject) => {
4445
request.login(user, (err) => {

components/server/src/iam/iam-session-app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export class IamSessionApp {
103103

104104
return {
105105
userId: user.id,
106+
newUser: !existingUser,
106107
};
107108
}
108109

0 commit comments

Comments
 (0)