Skip to content

Commit 747355e

Browse files
committed
✨(frontend) display onboarding modal when first connection
When the user connect for the first time, we display a onboarding modal, that explains the main functionnalities of Docs.
1 parent 1ff5b29 commit 747355e

File tree

6 files changed

+143
-2
lines changed

6 files changed

+143
-2
lines changed

src/backend/demo/management/commands/create_demo.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ def create_demo(stdout):
138138
password="!",
139139
is_superuser=False,
140140
is_active=True,
141+
is_first_connection=False,
141142
is_staff=False,
142143
short_name=first_name,
143144
full_name=f"{first_name:s} {random.choice(last_names):s}",
@@ -194,6 +195,7 @@ def create_demo(stdout):
194195
password="!",
195196
is_superuser=False,
196197
is_active=True,
198+
is_first_connection=False,
197199
is_staff=False,
198200
language=dev_user["language"] or random.choice(languages),
199201
)

src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ test.describe('Help feature', () => {
8181
);
8282
await learnMoreLink.click();
8383

84-
await page.getByRole('button', { name: /understood|compris/i }).click();
84+
await page.getByRole('button', { name: /understood/i }).click();
8585
await expect(modal).toBeHidden();
8686
});
8787

@@ -126,5 +126,52 @@ test.describe('Help feature', () => {
126126
modal.getByRole('button', { name: /Suivant/i }),
127127
).toBeVisible();
128128
});
129+
130+
test('Modal is displayed automatically on first connection', async ({
131+
page,
132+
browserName,
133+
}) => {
134+
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
135+
await expect(page.getByTestId('onboarding-modal')).toBeHidden();
136+
137+
await page.route(/.*\/api\/v1.0\/users\/me\//, async (route) => {
138+
const request = route.request();
139+
if (request.method().includes('GET')) {
140+
await route.fulfill({
141+
json: {
142+
id: 'f2bfcf0b-e4b9-4153-b2e5-0d2a9a5a0a5b',
143+
email: `user.test@${browserName.toLowerCase()}.test`,
144+
full_name: `E2E ${browserName}`,
145+
short_name: 'E2E',
146+
language: 'en-us',
147+
is_first_connection: true,
148+
},
149+
});
150+
} else {
151+
await route.continue();
152+
}
153+
});
154+
155+
let onboardingDoneCalled = false;
156+
await page.route(
157+
/.*\/api\/v1.0\/users\/onboarding-done\//,
158+
async (route) => {
159+
const request = route.request();
160+
if (request.method().includes('POST')) {
161+
onboardingDoneCalled = true;
162+
await route.continue();
163+
}
164+
},
165+
);
166+
167+
await page.goto('/');
168+
169+
await expect(page.getByTestId('onboarding-modal')).toBeVisible();
170+
171+
await page.getByRole('button', { name: /skip/i }).click();
172+
173+
await expect(page.getByTestId('onboarding-modal')).toBeHidden();
174+
expect(onboardingDoneCalled).toBeTruthy();
175+
});
129176
});
130177
});

src/frontend/apps/impress/src/features/auth/api/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99
export interface User {
1010
id: string;
11+
is_first_connection: boolean;
1112
email: string;
1213
full_name: string;
1314
short_name: string;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
UseMutationOptions,
3+
useMutation,
4+
useQueryClient,
5+
} from '@tanstack/react-query';
6+
7+
import { APIError, errorCauses, fetchAPI } from '@/api';
8+
9+
import { KEY_AUTH } from './useAuthQuery';
10+
import { User } from './types';
11+
12+
type OnboardingDoneResponse = {
13+
detail: string;
14+
};
15+
16+
export const onboardingDone = async (): Promise<OnboardingDoneResponse> => {
17+
const response = await fetchAPI(`users/onboarding-done/`, {
18+
method: 'POST',
19+
});
20+
21+
if (!response.ok) {
22+
throw new APIError(
23+
'Failed to complete onboarding',
24+
await errorCauses(response),
25+
);
26+
}
27+
28+
return response.json() as Promise<OnboardingDoneResponse>;
29+
};
30+
31+
type UseOnboardingDoneOptions = UseMutationOptions<
32+
OnboardingDoneResponse,
33+
APIError
34+
>;
35+
36+
export function useOnboardingDone(options?: UseOnboardingDoneOptions) {
37+
const queryClient = useQueryClient();
38+
return useMutation<OnboardingDoneResponse, APIError>({
39+
mutationFn: onboardingDone,
40+
onSuccess: (data, variables, onMutateResult, context) => {
41+
queryClient.setQueryData<User>([KEY_AUTH], (oldData) => {
42+
if (!oldData) {
43+
return oldData;
44+
}
45+
return {
46+
...oldData,
47+
is_first_connection: false,
48+
};
49+
});
50+
options?.onSuccess?.(data, variables, onMutateResult, context);
51+
},
52+
});
53+
}

src/frontend/apps/impress/src/features/auth/components/Auth.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ import {
1414
resetSilent,
1515
} from '../utils';
1616

17+
import { FirstConnection } from './FirstConnection';
18+
1719
export const Auth = ({ children }: PropsWithChildren) => {
1820
const {
1921
isLoading: isAuthLoading,
2022
pathAllowed,
2123
isFetchedAfterMount,
2224
authenticated,
2325
fetchStatus,
26+
user,
2427
} = useAuth();
2528
const isLoading = fetchStatus !== 'idle' || isAuthLoading;
2629
const [isRedirecting, setIsRedirecting] = useState(false);
@@ -110,5 +113,10 @@ export const Auth = ({ children }: PropsWithChildren) => {
110113
return <Loading $height="100vh" $width="100vw" />;
111114
}
112115

113-
return children;
116+
return (
117+
<>
118+
{children}
119+
{user && user.is_first_connection && <FirstConnection />}
120+
</>
121+
);
114122
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useModal } from '@gouvfr-lasuite/cunningham-react';
2+
import { useEffect } from 'react';
3+
4+
import { OnBoarding } from '@/features/help';
5+
6+
import { useOnboardingDone } from '../api/useOnboardingDone';
7+
8+
export const FirstConnection = () => {
9+
const modalOnbording = useModal();
10+
const { mutate: onboardingDone, isPending } = useOnboardingDone();
11+
12+
useEffect(() => {
13+
if (isPending) {
14+
return;
15+
}
16+
17+
modalOnbording.open();
18+
}, [modalOnbording, isPending]);
19+
20+
const onClose = () => {
21+
onboardingDone();
22+
modalOnbording.close();
23+
};
24+
25+
if (!modalOnbording.isOpen && isPending) {
26+
return null;
27+
}
28+
29+
return <OnBoarding isOpen={modalOnbording.isOpen} onClose={onClose} />;
30+
};

0 commit comments

Comments
 (0)