diff --git a/src/frontend/apps/impress/src/features/auth/__tests__/UserReconciliation.test.tsx b/src/frontend/apps/impress/src/features/auth/__tests__/UserReconciliation.test.tsx new file mode 100644 index 00000000..4f2d3959 --- /dev/null +++ b/src/frontend/apps/impress/src/features/auth/__tests__/UserReconciliation.test.tsx @@ -0,0 +1,62 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import fetchMock from 'fetch-mock'; +import React from 'react'; +import { describe, expect, test } from 'vitest'; + +import { AppWrapper } from '@/tests/utils'; + +import { UserReconciliation } from '../components/UserReconciliation'; + +describe('UserReconciliation', () => { + beforeEach(() => { + fetchMock.reset(); + }); + + ['active', 'inactive'].forEach((type) => { + test(`renders when reconciliation is a ${type} success`, async () => { + fetchMock.get( + `http://test.jest/api/v1.0/user_reconciliations/${type}/123456/`, + { details: 'Success' }, + ); + + render( + , + { + wrapper: AppWrapper, + }, + ); + + await waitFor(() => { + expect(fetchMock.calls().length).toBe(1); + }); + + expect( + await screen.findByText(/Email validated successfully !/i), + ).toBeInTheDocument(); + }); + }); + + test('renders when reconciliation fails', async () => { + fetchMock.get( + `http://test.jest/api/v1.0/user_reconciliations/active/invalid-id/`, + { + throws: new Error('invalid id'), + }, + ); + + render(, { + wrapper: AppWrapper, + }); + + await waitFor(() => { + expect(fetchMock.calls().length).toBe(1); + }); + + expect( + await screen.findByText(/An error occurred during email validation./i), + ).toBeInTheDocument(); + }); +}); diff --git a/src/frontend/apps/impress/src/features/auth/api/index.ts b/src/frontend/apps/impress/src/features/auth/api/index.ts index ce8db5d4..a07760a0 100644 --- a/src/frontend/apps/impress/src/features/auth/api/index.ts +++ b/src/frontend/apps/impress/src/features/auth/api/index.ts @@ -1,2 +1,3 @@ export * from './useAuthQuery'; +export * from './useUserReconciliations'; export * from './types'; diff --git a/src/frontend/apps/impress/src/features/auth/api/useUserReconciliations.tsx b/src/frontend/apps/impress/src/features/auth/api/useUserReconciliations.tsx new file mode 100644 index 00000000..9185563d --- /dev/null +++ b/src/frontend/apps/impress/src/features/auth/api/useUserReconciliations.tsx @@ -0,0 +1,51 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query'; + +import { APIError, errorCauses, fetchAPI } from '@/api'; + +type UserReconciliationResponse = { + details: string; +}; + +interface UserReconciliationProps { + type: 'active' | 'inactive'; + reconciliationId?: string; +} + +export const userReconciliations = async ({ + type, + reconciliationId, +}: UserReconciliationProps): Promise => { + const response = await fetchAPI( + `user_reconciliations/${type}/${reconciliationId}/`, + ); + + if (!response.ok) { + throw new APIError( + 'Failed to do the user reconciliation', + await errorCauses(response), + ); + } + + return response.json() as Promise; +}; + +export const KEY_USER_RECONCILIATIONS = 'user_reconciliations'; + +export function useUserReconciliationsQuery( + param: UserReconciliationProps, + queryConfig?: UseQueryOptions< + UserReconciliationResponse, + APIError, + UserReconciliationResponse + >, +) { + return useQuery< + UserReconciliationResponse, + APIError, + UserReconciliationResponse + >({ + queryKey: [KEY_USER_RECONCILIATIONS, param], + queryFn: () => userReconciliations(param), + ...queryConfig, + }); +} diff --git a/src/frontend/apps/impress/src/features/auth/assets/rocket.gif b/src/frontend/apps/impress/src/features/auth/assets/rocket.gif new file mode 100644 index 00000000..d0f18326 Binary files /dev/null and b/src/frontend/apps/impress/src/features/auth/assets/rocket.gif differ diff --git a/src/frontend/apps/impress/src/features/auth/components/UserReconciliation.tsx b/src/frontend/apps/impress/src/features/auth/components/UserReconciliation.tsx new file mode 100644 index 00000000..40a1e5e1 --- /dev/null +++ b/src/frontend/apps/impress/src/features/auth/components/UserReconciliation.tsx @@ -0,0 +1,104 @@ +import { Button } from '@gouvfr-lasuite/cunningham-react'; +import Image from 'next/image'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +import error_img from '@/assets/icons/error-planetes.png'; +import { Box, Icon, Loading, StyledLink, Text } from '@/components'; + +import { useUserReconciliationsQuery } from '../api'; +import success_gif from '../assets/rocket.gif'; + +const StyledButton = styled(Button)` + width: fit-content; +`; + +interface UserReconciliationProps { + type: 'active' | 'inactive'; + reconciliationId: string; +} + +export const UserReconciliation = ({ + type, + reconciliationId, +}: UserReconciliationProps) => { + const { t } = useTranslation(); + const { data: userReconciliations, isError } = useUserReconciliationsQuery({ + type, + reconciliationId, + }); + + if (!userReconciliations && !isError) { + return ( + + ); + } + + let render = ( + <> + + + {t('Email validated successfully !')} + + + ); + + if (isError) { + render = ( + <> + + + {t('An error occurred during email validation.')} + + + ); + } + + return ( + + {render} + + + + + } + > + {t('Home')} + + + + + ); +}; diff --git a/src/frontend/apps/impress/src/features/auth/components/index.ts b/src/frontend/apps/impress/src/features/auth/components/index.ts index 26ebaf2e..0a91bf37 100644 --- a/src/frontend/apps/impress/src/features/auth/components/index.ts +++ b/src/frontend/apps/impress/src/features/auth/components/index.ts @@ -1,3 +1,4 @@ export * from './Auth'; export * from './ButtonLogin'; export * from './UserAvatar'; +export * from './UserReconciliation'; diff --git a/src/frontend/apps/impress/src/pages/user_reconciliations/active/[id]/index.tsx b/src/frontend/apps/impress/src/pages/user_reconciliations/active/[id]/index.tsx new file mode 100644 index 00000000..7fa8c27a --- /dev/null +++ b/src/frontend/apps/impress/src/pages/user_reconciliations/active/[id]/index.tsx @@ -0,0 +1,40 @@ +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { UserReconciliation } from '@/features/auth/components/UserReconciliation'; +import { PageLayout } from '@/layouts'; +import { NextPageWithLayout } from '@/types/next'; + +const Page: NextPageWithLayout = () => { + const { t } = useTranslation(); + const { + query: { id }, + } = useRouter(); + + if (typeof id !== 'string') { + return null; + } + + return ( + <> + + + {`${t('User reconciliation')} - ${t('Docs')}`} + + + + + ); +}; + +Page.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Page; diff --git a/src/frontend/apps/impress/src/pages/user_reconciliations/inactive/[id]/index.tsx b/src/frontend/apps/impress/src/pages/user_reconciliations/inactive/[id]/index.tsx new file mode 100644 index 00000000..c10cbb38 --- /dev/null +++ b/src/frontend/apps/impress/src/pages/user_reconciliations/inactive/[id]/index.tsx @@ -0,0 +1,40 @@ +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { UserReconciliation } from '@/features/auth/components/UserReconciliation'; +import { PageLayout } from '@/layouts'; +import { NextPageWithLayout } from '@/types/next'; + +const Page: NextPageWithLayout = () => { + const { t } = useTranslation(); + const { + query: { id }, + } = useRouter(); + + if (typeof id !== 'string') { + return null; + } + + return ( + <> + + + {`${t('User reconciliation')} - ${t('Docs')}`} + + + + + ); +}; + +Page.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Page; diff --git a/src/frontend/apps/impress/vitest.setup.ts b/src/frontend/apps/impress/vitest.setup.ts index de0945cf..6e449e8b 100644 --- a/src/frontend/apps/impress/vitest.setup.ts +++ b/src/frontend/apps/impress/vitest.setup.ts @@ -1,4 +1,23 @@ import '@testing-library/jest-dom/vitest'; import * as dotenv from 'dotenv'; +import React from 'react'; +import { vi } from 'vitest'; dotenv.config({ path: './.env.test', quiet: true }); + +vi.mock('next/image', () => ({ + __esModule: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + default: (props: any) => { + const { + src, + alt = '', + unoptimized: _unoptimized, + priority: _priority, + ...rest + } = props; + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const resolved = typeof src === 'string' ? src : src?.src; + return React.createElement('img', { src: resolved, alt, ...rest }); + }, +}));