✨(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.
This commit is contained in:
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
is_first_connection: boolean;
|
||||
email: string;
|
||||
full_name: string;
|
||||
short_name: string;
|
||||
|
||||
@@ -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<OnboardingDoneResponse> => {
|
||||
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<OnboardingDoneResponse>;
|
||||
};
|
||||
|
||||
type UseOnboardingDoneOptions = UseMutationOptions<
|
||||
OnboardingDoneResponse,
|
||||
APIError
|
||||
>;
|
||||
|
||||
export function useOnboardingDone(options?: UseOnboardingDoneOptions) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<OnboardingDoneResponse, APIError>({
|
||||
mutationFn: onboardingDone,
|
||||
onSuccess: (data, variables, onMutateResult, context) => {
|
||||
queryClient.setQueryData<User>([KEY_AUTH], (oldData) => {
|
||||
if (!oldData) {
|
||||
return oldData;
|
||||
}
|
||||
return {
|
||||
...oldData,
|
||||
is_first_connection: false,
|
||||
};
|
||||
});
|
||||
options?.onSuccess?.(data, variables, onMutateResult, context);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 <Loading $height="100vh" $width="100vw" />;
|
||||
}
|
||||
|
||||
return children;
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{user && user.is_first_connection && <FirstConnection />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 <OnBoarding isOpen={modalOnbording.isOpen} onClose={onClose} />;
|
||||
};
|
||||
+1
@@ -35,6 +35,7 @@ export const DocShareInvitationItem = ({
|
||||
email: invitation.email,
|
||||
short_name: invitation.email,
|
||||
language: 'en-us',
|
||||
is_first_connection: false,
|
||||
};
|
||||
|
||||
const { toast } = useToastProvider();
|
||||
|
||||
@@ -353,6 +353,7 @@ const QuickSearchInviteInputSection = ({
|
||||
email: userQuery,
|
||||
short_name: '',
|
||||
language: '',
|
||||
is_first_connection: false,
|
||||
};
|
||||
|
||||
const hasEmailInUsers = users.some(
|
||||
|
||||
Reference in New Issue
Block a user