Skip to content

Commit 517ea1f

Browse files
committed
feat: navigate between steps + dismiss
1 parent f5cdb19 commit 517ea1f

File tree

9 files changed

+198
-39
lines changed

9 files changed

+198
-39
lines changed

studio/src/components/layout/onboarding-layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ export interface OnboardingLayoutProps {
44

55
export const OnboardingLayout = ({ children }: OnboardingLayoutProps) => {
66
return (
7-
<div className="mx-auto min-h-screen w-full bg-background font-sans antialiased">
8-
<main className="flex-1">{children}</main>
7+
<div className="flex min-h-screen w-full flex-col items-center justify-center bg-background font-sans antialiased">
8+
<main className="w-full max-w-lg px-4">{children}</main>
99
</div>
1010
);
1111
};

studio/src/components/onboarding/onboarding-provider.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import {
22
createContext,
33
type Dispatch,
4+
useCallback,
45
useContext,
56
useMemo,
67
useState,
78
type SetStateAction,
89
type ReactNode,
910
} from 'react';
1011
import { PostHogFeatureFlagContext } from '../posthog-feature-flag-provider';
12+
import { useSessionStorage } from '@/hooks/use-session-storage';
1113

1214
type Onboarding = {
13-
step: number;
1415
finishedAt?: Date;
1516
federatedGraphsCount: number;
1617
};
@@ -19,25 +20,66 @@ export interface OnboardingState {
1920
enabled: boolean;
2021
onboarding?: Onboarding;
2122
setOnboarding: Dispatch<SetStateAction<Onboarding | undefined>>;
23+
currentStep: number | undefined;
24+
setStep: (step: number | undefined) => void;
25+
skipped: boolean;
26+
setSkipped: () => void;
27+
resetSkipped: () => void;
2228
}
2329

2430
export const OnboardingContext = createContext<OnboardingState>({
2531
onboarding: undefined,
2632
enabled: false,
2733
setOnboarding: () => undefined,
34+
currentStep: undefined,
35+
setStep: () => undefined,
36+
skipped: false,
37+
setSkipped: () => undefined,
38+
resetSkipped: () => undefined,
2839
});
2940

41+
const ONBOARDING_V1_LAST_STEP = 4;
42+
3043
export const OnboardingProvider = ({ children }: { children: ReactNode }) => {
3144
const { onboarding: onboardingFlag, status: featureFlagStatus } = useContext(PostHogFeatureFlagContext);
3245
const [onboarding, setOnboarding] = useState<Onboarding | undefined>(undefined);
46+
const [currentStep, setCurrentStep] = useSessionStorage<undefined | number>('cosmo-onboarding-v1-step', undefined);
47+
const [skipped, setSkippedValue] = useSessionStorage('cosmo-onboarding-v1-skipped', false);
48+
49+
const setSkipped = useCallback(() => {
50+
setSkippedValue(true);
51+
}, [setSkippedValue]);
52+
53+
const resetSkipped = useCallback(() => {
54+
setSkippedValue(false);
55+
}, [setSkippedValue]);
56+
57+
const setStep = useCallback(
58+
(step: number | undefined) => {
59+
if (step === undefined) {
60+
setCurrentStep(1);
61+
resetSkipped();
62+
return;
63+
}
64+
65+
resetSkipped();
66+
setCurrentStep(Math.max(Math.min(step, ONBOARDING_V1_LAST_STEP), 0));
67+
},
68+
[setCurrentStep, resetSkipped],
69+
);
3370

3471
const value = useMemo(
3572
() => ({
3673
onboarding,
3774
enabled: Boolean(onboardingFlag.enabled && featureFlagStatus === 'success' && onboardingFlag),
3875
setOnboarding,
76+
currentStep,
77+
setStep,
78+
setSkipped,
79+
resetSkipped,
80+
skipped,
3981
}),
40-
[onboarding, onboardingFlag, featureFlagStatus],
82+
[onboarding, onboardingFlag, featureFlagStatus, currentStep, setStep, setSkipped, resetSkipped, skipped],
4183
);
4284

4385
return <OnboardingContext.Provider value={value}>{children}</OnboardingContext.Provider>;
Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
import { useEffect } from 'react';
2+
import { Link } from '../ui/link';
3+
import { Button } from '../ui/button';
4+
import { useOnboarding } from '@/hooks/use-onboarding';
5+
16
export const Step1 = () => {
2-
return <h2>Step 1</h2>;
7+
const { setStep, setSkipped } = useOnboarding();
8+
9+
useEffect(() => {
10+
setStep(1);
11+
}, [setStep]);
12+
13+
return (
14+
<div className="flex flex-col items-center gap-4 text-center">
15+
<h2 className="text-2xl font-semibold tracking-tight">Step 1</h2>
16+
<div className="flex w-full justify-between">
17+
<Button asChild variant="secondary" onClick={setSkipped}>
18+
<Link href="/">Skip</Link>
19+
</Button>
20+
<div className="flex">
21+
<Button className="mr-2" asChild disabled>
22+
<Link href="#">Back</Link>
23+
</Button>
24+
<Button asChild>
25+
<Link href="/onboarding/2">Next</Link>
26+
</Button>
27+
</div>
28+
</div>
29+
</div>
30+
);
331
};
Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
import { useEffect } from 'react';
2+
import { Link } from '../ui/link';
3+
import { Button } from '../ui/button';
4+
import { useOnboarding } from '@/hooks/use-onboarding';
5+
16
export const Step2 = () => {
2-
return <h2>Step 2</h2>;
7+
const { setStep, setSkipped } = useOnboarding();
8+
9+
useEffect(() => {
10+
setStep(2);
11+
}, [setStep]);
12+
13+
return (
14+
<div className="flex flex-col items-center gap-4 text-center">
15+
<h2 className="text-2xl font-semibold tracking-tight">Step 2</h2>
16+
<div className="flex w-full justify-between">
17+
<Button asChild variant="secondary" onClick={setSkipped}>
18+
<Link href="/">Skip</Link>
19+
</Button>
20+
<div className="flex">
21+
<Button className="mr-2" asChild>
22+
<Link href="/onboarding/1">Back</Link>
23+
</Button>
24+
<Button asChild>
25+
<Link href="/onboarding/3">Next</Link>
26+
</Button>
27+
</div>
28+
</div>
29+
</div>
30+
);
331
};
Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
import { useEffect } from 'react';
2+
import { Link } from '../ui/link';
3+
import { Button } from '../ui/button';
4+
import { useOnboarding } from '@/hooks/use-onboarding';
5+
16
export const Step3 = () => {
2-
return <h2>Step 3</h2>;
7+
const { setStep, setSkipped } = useOnboarding();
8+
9+
useEffect(() => {
10+
setStep(3);
11+
}, [setStep]);
12+
13+
return (
14+
<div className="flex flex-col items-center gap-4 text-center">
15+
<h2 className="text-2xl font-semibold tracking-tight">Step 3</h2>
16+
<div className="flex w-full justify-between">
17+
<Button asChild variant="secondary" onClick={setSkipped}>
18+
<Link href="/">Skip</Link>
19+
</Button>
20+
<div className="flex">
21+
<Button className="mr-2" asChild>
22+
<Link href="/onboarding/2">Back</Link>
23+
</Button>
24+
<Button asChild>
25+
<Link href="/onboarding/4">Next</Link>
26+
</Button>
27+
</div>
28+
</div>
29+
</div>
30+
);
331
};
Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
import { useEffect } from 'react';
2+
import { Link } from '../ui/link';
3+
import { Button } from '../ui/button';
4+
import { useOnboarding } from '@/hooks/use-onboarding';
5+
16
export const Step4 = () => {
2-
return <h2>Step 4</h2>;
7+
const { setStep, setSkipped } = useOnboarding();
8+
9+
useEffect(() => {
10+
setStep(3);
11+
}, [setStep]);
12+
13+
return (
14+
<div className="flex flex-col items-center gap-4 text-center">
15+
<h2 className="text-2xl font-semibold tracking-tight">Step 4</h2>
16+
<div className="flex w-full justify-between">
17+
<Button asChild variant="secondary" onClick={setSkipped}>
18+
<Link href="/">Skip</Link>
19+
</Button>
20+
<div className="flex">
21+
<Button className="mr-2" asChild>
22+
<Link href="/onboarding/3">Back</Link>
23+
</Button>
24+
<Button asChild>
25+
<Link href="/">Finish</Link>
26+
</Button>
27+
</div>
28+
</div>
29+
</div>
30+
);
331
};
Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useRef, useState } from 'react';
1+
import { useEffect, useRef, useMemo } from 'react';
22
import Router from 'next/router';
33
import { useOnboarding } from './use-onboarding';
44
import { useQuery } from '@connectrpc/connect-query';
@@ -10,57 +10,57 @@ import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb
1010
* the conditions based on feature flag and onboarding metadata
1111
*/
1212
export const useOnboardingNavigation = () => {
13-
const { enabled, onboarding, setOnboarding } = useOnboarding();
14-
const { data, isError, isPending, error } = useQuery(getOnboarding);
15-
const [initialLoadSuccess, setInitialLoadSuccess] = useState(false);
13+
const { enabled, setOnboarding, skipped, currentStep } = useOnboarding();
14+
const { data, isError, isPending } = useQuery(getOnboarding);
15+
const initialRedirect = useRef<boolean>(false);
1616

17-
useEffect(
18-
function initialOnboardingFetch() {
19-
if (isPending) {
20-
return;
21-
}
17+
const initialLoadSuccess = useMemo(() => {
18+
if (isPending) return null;
19+
if (isError || data?.response?.code !== EnumStatusCode.OK) return false;
20+
return true;
21+
}, [isPending, isError, data]);
2222

23-
if (isError || data?.response?.code !== EnumStatusCode.OK) {
24-
setInitialLoadSuccess(false);
23+
useEffect(
24+
function syncOnboardingMetadata() {
25+
if (initialLoadSuccess !== true || !data) {
2526
return;
2627
}
2728

28-
setInitialLoadSuccess(true);
2929
setOnboarding({
30-
step: Number(data.step ?? 0),
3130
finishedAt: data.finishedAt ? new Date(data.finishedAt) : undefined,
3231
federatedGraphsCount: data.federatedGraphsCount,
3332
});
3433
},
35-
[data, isError, isPending, setOnboarding, error],
34+
[initialLoadSuccess, data, setOnboarding],
3635
);
3736

3837
useEffect(
3938
function handleNavigationToOnboarding() {
40-
// Redirect user back if onboarding metadata failed
41-
if (!initialLoadSuccess && Router.pathname.startsWith('/onboarding')) {
42-
Router.replace('/');
39+
// Wait for the onboarding metadata query to resolve
40+
// Do not initiate redirect if we fail to fetch onboarding metadata. Fail silently in background.
41+
if (initialLoadSuccess === null || !initialLoadSuccess) {
4342
return;
4443
}
4544

46-
// Do not initiate redirect if we fail to fetch onboarding metadata. Fail silently in background.
47-
if (!initialLoadSuccess) {
45+
// If user has dissmissed/skipped the onboarding but, do not redirect
46+
if (skipped) {
4847
return;
4948
}
5049

51-
// Do not initiate redirect if the user is not eligible for onboarding
52-
if (!onboarding && !enabled) {
50+
// If user has already finished the onboarding, don't redirect
51+
if (data?.finishedAt) {
5352
return;
5453
}
5554

56-
// Do not initiate redirect if user has already finished the onboarding
57-
if (onboarding?.finishedAt) {
55+
// skip redirecting on subsequent re-runs of this functions
56+
if (initialRedirect.current) {
5857
return;
5958
}
6059

61-
const path = onboarding ? `/onboarding/${onboarding.step}` : '/onboarding';
60+
const path = currentStep ? `/onboarding/${currentStep}` : `/onboarding/1`;
61+
initialRedirect.current = true;
6262
Router.replace(path);
6363
},
64-
[onboarding, enabled, initialLoadSuccess],
64+
[data, enabled, initialLoadSuccess, skipped, currentStep],
6565
);
6666
};

studio/src/hooks/use-session-storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ declare global {
99
}
1010
}
1111

12-
type SetValue<T> = Dispatch<SetStateAction<T>>;
12+
export type SetValue<T> = Dispatch<SetStateAction<T>>;
1313

1414
export function useSessionStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
1515
// Get from session storage then

studio/src/pages/onboarding/index.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1+
import { useRef } from 'react';
12
import { OnboardingLayout } from '@/components/layout/onboarding-layout';
23
import { NextPageWithLayout } from '@/lib/page';
3-
import { useRouter } from 'next/router';
4+
import Router from 'next/router';
45
import { useEffect } from 'react';
6+
import { Loader } from '@/components/ui/loader';
7+
import { useOnboarding } from '@/hooks/use-onboarding';
58

69
const OnboardingIndex: NextPageWithLayout = () => {
7-
const router = useRouter();
10+
const { currentStep } = useOnboarding();
11+
const initialStep = useRef(currentStep);
812

913
useEffect(() => {
10-
router.replace('/onboarding/1');
11-
}, [router]);
14+
// Only redirect user when they first enter
15+
Router.replace(initialStep.current ? `/onboarding/${initialStep.current}` : '/onboarding/1');
16+
}, []);
1217

13-
return null;
18+
return <Loader />;
1419
};
1520

1621
OnboardingIndex.getLayout = (page) => <OnboardingLayout>{page}</OnboardingLayout>;

0 commit comments

Comments
 (0)