From 3eed5428003fd24a6108bcae77b05ac9ef21aba4 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Fri, 13 Mar 2026 16:24:19 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20display=20onboarding=20mo?= =?UTF-8?q?dal=20when=20first=20connection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user connect for the first time, we display a onboarding modal, that explains the main functionnalities of Docs. --- .../demo/management/commands/create_demo.py | 2 + .../e2e/__tests__/app-impress/help.spec.ts | 49 ++++++++++++++++- .../impress/src/features/auth/api/types.ts | 1 + .../features/auth/api/useOnboardingDone.tsx | 53 +++++++++++++++++++ .../src/features/auth/components/Auth.tsx | 10 +++- .../auth/components/FirstConnection.tsx | 30 +++++++++++ .../components/DocShareInvitation.tsx | 1 + .../doc-share/components/DocShareModal.tsx | 1 + 8 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/auth/api/useOnboardingDone.tsx create mode 100644 src/frontend/apps/impress/src/features/auth/components/FirstConnection.tsx diff --git a/src/backend/demo/management/commands/create_demo.py b/src/backend/demo/management/commands/create_demo.py index 695e2a4a..e216edf9 100644 --- a/src/backend/demo/management/commands/create_demo.py +++ b/src/backend/demo/management/commands/create_demo.py @@ -138,6 +138,7 @@ def create_demo(stdout): password="!", is_superuser=False, is_active=True, + is_first_connection=False, is_staff=False, short_name=first_name, full_name=f"{first_name:s} {random.choice(last_names):s}", @@ -194,6 +195,7 @@ def create_demo(stdout): password="!", is_superuser=False, is_active=True, + is_first_connection=False, is_staff=False, language=dev_user["language"] or random.choice(languages), ) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts index 1ee405de..5abb99a8 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/help.spec.ts @@ -81,7 +81,7 @@ test.describe('Help feature', () => { ); await learnMoreLink.click(); - await page.getByRole('button', { name: /understood|compris/i }).click(); + await page.getByRole('button', { name: /understood/i }).click(); await expect(modal).toBeHidden(); }); @@ -126,5 +126,52 @@ test.describe('Help feature', () => { modal.getByRole('button', { name: /Suivant/i }), ).toBeVisible(); }); + + test('Modal is displayed automatically on first connection', async ({ + page, + browserName, + }) => { + await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible(); + await expect(page.getByTestId('onboarding-modal')).toBeHidden(); + + await page.route(/.*\/api\/v1.0\/users\/me\//, async (route) => { + const request = route.request(); + if (request.method().includes('GET')) { + await route.fulfill({ + json: { + id: 'f2bfcf0b-e4b9-4153-b2e5-0d2a9a5a0a5b', + email: `user.test@${browserName.toLowerCase()}.test`, + full_name: `E2E ${browserName}`, + short_name: 'E2E', + language: 'en-us', + is_first_connection: true, + }, + }); + } else { + await route.continue(); + } + }); + + let onboardingDoneCalled = false; + await page.route( + /.*\/api\/v1.0\/users\/onboarding-done\//, + async (route) => { + const request = route.request(); + if (request.method().includes('POST')) { + onboardingDoneCalled = true; + await route.continue(); + } + }, + ); + + await page.goto('/'); + + await expect(page.getByTestId('onboarding-modal')).toBeVisible(); + + await page.getByRole('button', { name: /skip/i }).click(); + + await expect(page.getByTestId('onboarding-modal')).toBeHidden(); + expect(onboardingDoneCalled).toBeTruthy(); + }); }); }); diff --git a/src/frontend/apps/impress/src/features/auth/api/types.ts b/src/frontend/apps/impress/src/features/auth/api/types.ts index 75a46581..6234729d 100644 --- a/src/frontend/apps/impress/src/features/auth/api/types.ts +++ b/src/frontend/apps/impress/src/features/auth/api/types.ts @@ -8,6 +8,7 @@ */ export interface User { id: string; + is_first_connection: boolean; email: string; full_name: string; short_name: string; diff --git a/src/frontend/apps/impress/src/features/auth/api/useOnboardingDone.tsx b/src/frontend/apps/impress/src/features/auth/api/useOnboardingDone.tsx new file mode 100644 index 00000000..b694b3b2 --- /dev/null +++ b/src/frontend/apps/impress/src/features/auth/api/useOnboardingDone.tsx @@ -0,0 +1,53 @@ +import { + UseMutationOptions, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; + +import { APIError, errorCauses, fetchAPI } from '@/api'; + +import { User } from './types'; +import { KEY_AUTH } from './useAuthQuery'; + +type OnboardingDoneResponse = { + detail: string; +}; + +export const onboardingDone = async (): Promise => { + const response = await fetchAPI(`users/onboarding-done/`, { + method: 'POST', + }); + + if (!response.ok) { + throw new APIError( + 'Failed to complete onboarding', + await errorCauses(response), + ); + } + + return response.json() as Promise; +}; + +type UseOnboardingDoneOptions = UseMutationOptions< + OnboardingDoneResponse, + APIError +>; + +export function useOnboardingDone(options?: UseOnboardingDoneOptions) { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: onboardingDone, + onSuccess: (data, variables, onMutateResult, context) => { + queryClient.setQueryData([KEY_AUTH], (oldData) => { + if (!oldData) { + return oldData; + } + return { + ...oldData, + is_first_connection: false, + }; + }); + options?.onSuccess?.(data, variables, onMutateResult, context); + }, + }); +} diff --git a/src/frontend/apps/impress/src/features/auth/components/Auth.tsx b/src/frontend/apps/impress/src/features/auth/components/Auth.tsx index abeaf4ff..178474ab 100644 --- a/src/frontend/apps/impress/src/features/auth/components/Auth.tsx +++ b/src/frontend/apps/impress/src/features/auth/components/Auth.tsx @@ -14,6 +14,8 @@ import { resetSilent, } from '../utils'; +import { FirstConnection } from './FirstConnection'; + export const Auth = ({ children }: PropsWithChildren) => { const { isLoading: isAuthLoading, @@ -21,6 +23,7 @@ export const Auth = ({ children }: PropsWithChildren) => { isFetchedAfterMount, authenticated, fetchStatus, + user, } = useAuth(); const isLoading = fetchStatus !== 'idle' || isAuthLoading; const [isRedirecting, setIsRedirecting] = useState(false); @@ -110,5 +113,10 @@ export const Auth = ({ children }: PropsWithChildren) => { return ; } - return children; + return ( + <> + {children} + {user && user.is_first_connection && } + + ); }; diff --git a/src/frontend/apps/impress/src/features/auth/components/FirstConnection.tsx b/src/frontend/apps/impress/src/features/auth/components/FirstConnection.tsx new file mode 100644 index 00000000..bb5e3ae6 --- /dev/null +++ b/src/frontend/apps/impress/src/features/auth/components/FirstConnection.tsx @@ -0,0 +1,30 @@ +import { useModal } from '@gouvfr-lasuite/cunningham-react'; +import { useEffect } from 'react'; + +import { OnBoarding } from '@/features/help'; + +import { useOnboardingDone } from '../api/useOnboardingDone'; + +export const FirstConnection = () => { + const modalOnbording = useModal(); + const { mutate: onboardingDone, isPending } = useOnboardingDone(); + + useEffect(() => { + if (isPending) { + return; + } + + modalOnbording.open(); + }, [modalOnbording, isPending]); + + const onClose = () => { + onboardingDone(); + modalOnbording.close(); + }; + + if (!modalOnbording.isOpen && isPending) { + return null; + } + + return ; +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx index a281d1e5..721bf5bd 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx @@ -35,6 +35,7 @@ export const DocShareInvitationItem = ({ email: invitation.email, short_name: invitation.email, language: 'en-us', + is_first_connection: false, }; const { toast } = useToastProvider(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx index 2662fff0..c6328542 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx @@ -353,6 +353,7 @@ const QuickSearchInviteInputSection = ({ email: userQuery, short_name: '', language: '', + is_first_connection: false, }; const hasEmailInUsers = users.some(