diff --git a/packages/webapp-libs/webapp-api-client/src/api/auth/auth.hooks.ts b/packages/webapp-libs/webapp-api-client/src/api/auth/auth.hooks.ts index 3f75d40d7..3f6ae552a 100644 --- a/packages/webapp-libs/webapp-api-client/src/api/auth/auth.hooks.ts +++ b/packages/webapp-libs/webapp-api-client/src/api/auth/auth.hooks.ts @@ -5,8 +5,8 @@ import { useCallback } from 'react'; import { apiURL } from '../helpers'; import { OAuthProvider } from './auth.types'; -export const getOauthUrl = (provider: OAuthProvider, locale?: string) => - apiURL(`/auth/social/login/${provider}?next=${encodeURIComponent(window.location.origin)}&locale=${locale ?? 'en'}`); +export const getOauthUrl = (provider: OAuthProvider, locale = 'en') => + apiURL(`/auth/social/login/${provider}?next=${encodeURIComponent(window.location.href)}&locale=${locale}`); export const useOAuthLogin = () => { const { diff --git a/packages/webapp/src/shared/components/auth/loginForm/__tests__/loginForm.component.spec.tsx b/packages/webapp/src/shared/components/auth/loginForm/__tests__/loginForm.component.spec.tsx index ad3668e5b..9c60525ed 100644 --- a/packages/webapp/src/shared/components/auth/loginForm/__tests__/loginForm.component.spec.tsx +++ b/packages/webapp/src/shared/components/auth/loginForm/__tests__/loginForm.component.spec.tsx @@ -12,11 +12,13 @@ import { authSinginMutation } from '../loginForm.graphql'; jest.mock('@sb/webapp-core/services/analytics'); +const mockSearch = '?redirect=%2Fen%2Fprofile'; const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => { return { ...jest.requireActual('react-router-dom'), useNavigate: () => mockNavigate, + useLocation: () => ({ search: mockSearch }), }; }); const Component = () => ; @@ -34,14 +36,42 @@ describe('LoginForm: Component', () => { const getEmailInput = async () => await screen.findByLabelText(/email/i); const getPasswordInput = async () => await screen.findByLabelText(/password/i); - const clickLoginButton = async () => userEvent.click(await screen.findByRole('button', { name: /log in/i })); + const clickLoginButton = async () => await userEvent.click(await screen.findByRole('button', { name: /log in/i })); const user = currentUserFactory({ firstName: 'Jack', lastName: 'White', email: 'jack.white@mail.com', + roles: [Role.USER], }); + it('should redirect with searchParams if otp available', async () => { + const refreshQueryMock = fillCommonQueryWithUser(user); + const requestMock = composeMockedQueryResult(authSinginMutation, { + variables: mockCredentials, + data: { + tokenAuth: { + access: 'access-token', + refresh: 'refresh-token', + otpAuthToken: 'otpAuthToken', + }, + }, + }); + + const { waitForApolloMocks } = render(, { + apolloMocks: [requestMock, refreshQueryMock], + }); + await waitForApolloMocks(); + + await userEvent.type(await getEmailInput(), mockCredentials.input.email); + await userEvent.type(await getPasswordInput(), mockCredentials.input.password); + + await clickLoginButton(); + + expect(trackEvent).not.toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledWith({ pathname: '/en/auth/validate-otp', search: mockSearch }); + }); + it('should call login action when submitted', async () => { const refreshQueryMock = fillCommonQueryWithUser(user); const requestMock = composeMockedQueryResult(authSinginMutation, { @@ -65,7 +95,6 @@ describe('LoginForm: Component', () => { await waitForApolloMocks(); expect(trackEvent).toHaveBeenCalledWith('auth', 'log-in'); - expect(await mockNavigate).toHaveBeenCalledWith(`/en`); }); it('should show error if required value is missing', async () => { @@ -76,7 +105,6 @@ describe('LoginForm: Component', () => { await clickLoginButton(); expect(await screen.findByText('Password is required')).toBeInTheDocument(); - expect(await mockNavigate).not.toHaveBeenCalled(); }); it('should show generic form error if action throws error', async () => { diff --git a/packages/webapp/src/shared/components/auth/loginForm/loginForm.hooks.ts b/packages/webapp/src/shared/components/auth/loginForm/loginForm.hooks.ts index 063e5848f..af3893a3e 100644 --- a/packages/webapp/src/shared/components/auth/loginForm/loginForm.hooks.ts +++ b/packages/webapp/src/shared/components/auth/loginForm/loginForm.hooks.ts @@ -5,7 +5,7 @@ import { useCommonQuery } from '@sb/webapp-api-client/providers'; import { useGenerateLocalePath } from '@sb/webapp-core/hooks'; import { trackEvent } from '@sb/webapp-core/services/analytics'; import { useIntl } from 'react-intl'; -import { useNavigate } from 'react-router-dom'; +import { useLocation, useNavigate } from 'react-router-dom'; import { RoutesConfig } from '../../../../app/config/routes'; import { authSinginMutation } from './loginForm.graphql'; @@ -16,6 +16,7 @@ export const useLoginForm = () => { const navigate = useNavigate(); const generateLocalePath = useGenerateLocalePath(); const { reload: reloadCommonQuery } = useCommonQuery(); + const { search } = useLocation(); const form = useApiForm({ errorMessages: { @@ -36,14 +37,15 @@ export const useLoginForm = () => { const [commitLoginMutation, { loading }] = useMutation(authSinginMutation, { onCompleted: ({ tokenAuth }) => { if (tokenAuth?.otpAuthToken) { - return navigate(generateLocalePath(RoutesConfig.validateOtp)); + return navigate({ + pathname: generateLocalePath(RoutesConfig.validateOtp), + search: search || undefined, + }); } reloadCommonQuery(); trackEvent('auth', 'log-in'); - - navigate(generateLocalePath(RoutesConfig.home)); }, onError: (error) => { setApolloGraphQLResponseErrors(error.graphQLErrors); diff --git a/packages/webapp/src/shared/components/auth/validateOtpForm/__tests__/validateOtpForm.component.spec.tsx b/packages/webapp/src/shared/components/auth/validateOtpForm/__tests__/validateOtpForm.component.spec.tsx index 8ae3ba82b..dcf76cf96 100644 --- a/packages/webapp/src/shared/components/auth/validateOtpForm/__tests__/validateOtpForm.component.spec.tsx +++ b/packages/webapp/src/shared/components/auth/validateOtpForm/__tests__/validateOtpForm.component.spec.tsx @@ -31,7 +31,7 @@ describe('ValidateOtpForm: Component', () => { mockNavigate.mockReset(); }); - it('should redirect after successful validation', async () => { + it('should call trackEvent after successful validation', async () => { const token = '331553'; const requestMock = composeMockedQueryResult(validateOtpMutation, { variables: { input: { otpToken: token } }, @@ -51,7 +51,6 @@ describe('ValidateOtpForm: Component', () => { await userEvent.click(submitButton); await waitForApolloMocks(); - expect(mockNavigate).toHaveBeenCalledWith(`/en`); expect(trackEvent).toHaveBeenCalledWith('auth', 'otp-validate'); }); diff --git a/packages/webapp/src/shared/components/auth/validateOtpForm/validateOtpForm.component.tsx b/packages/webapp/src/shared/components/auth/validateOtpForm/validateOtpForm.component.tsx index 78bd212bb..eff1e286f 100644 --- a/packages/webapp/src/shared/components/auth/validateOtpForm/validateOtpForm.component.tsx +++ b/packages/webapp/src/shared/components/auth/validateOtpForm/validateOtpForm.component.tsx @@ -4,12 +4,9 @@ import { useCommonQuery } from '@sb/webapp-api-client/providers'; import { Button, ButtonSize } from '@sb/webapp-core/components/buttons'; import { Input } from '@sb/webapp-core/components/forms'; import { H3, Small } from '@sb/webapp-core/components/typography'; -import { useGenerateLocalePath } from '@sb/webapp-core/hooks'; import { trackEvent } from '@sb/webapp-core/services/analytics'; import { FormattedMessage, useIntl } from 'react-intl'; -import { useNavigate } from 'react-router-dom'; -import { RoutesConfig } from '../../../../app/config/routes'; import { validateOtpMutation } from '../twoFactorAuthForm/twoFactorAuthForm.graphql'; export type ValidateOtpFormFields = { @@ -18,9 +15,7 @@ export type ValidateOtpFormFields = { export const ValidateOtpForm = () => { const intl = useIntl(); - const navigate = useNavigate(); const form = useApiForm(); - const generateLocalePath = useGenerateLocalePath(); const { handleSubmit, hasGenericErrorOnly, @@ -43,7 +38,6 @@ export const ValidateOtpForm = () => { const { data } = await commitValidateOtpMutation({ variables: { input: { otpToken: values.token } } }); if (data?.validateOtp?.access) { reloadCommonQuery(); - navigate(generateLocalePath(RoutesConfig.home)); } }; diff --git a/packages/webapp/src/shared/components/routes/anonymousRoute/__tests__/anonymousRoute.component.spec.tsx b/packages/webapp/src/shared/components/routes/anonymousRoute/__tests__/anonymousRoute.component.spec.tsx index 997836943..f62b532c5 100644 --- a/packages/webapp/src/shared/components/routes/anonymousRoute/__tests__/anonymousRoute.component.spec.tsx +++ b/packages/webapp/src/shared/components/routes/anonymousRoute/__tests__/anonymousRoute.component.spec.tsx @@ -14,6 +14,7 @@ describe('AnonymousRoute: Component', () => { } /> } /> + } /> ); @@ -39,5 +40,24 @@ describe('AnonymousRoute: Component', () => { expect(screen.queryByTestId('content')).not.toBeInTheDocument(); expect(screen.queryByTestId('home-content')).not.toBeInTheDocument(); }); + + it('should redirect to redirectUrl', async () => { + const mockSearch = 'en/profile'; + const spy = jest.spyOn(URLSearchParams.prototype, 'get').mockImplementation((key) => mockSearch); + + const apolloMocks = [ + fillCommonQueryWithUser( + currentUserFactory({ + roles: [Role.ADMIN], + }) + ), + ]; + + const { waitForApolloMocks } = render(, { apolloMocks }); + await waitForApolloMocks(0); + expect(screen.queryByTestId('content')).not.toBeInTheDocument(); + expect(screen.getByTestId('profile-content')).toBeInTheDocument(); + spy.mockClear(); + }); }); }); diff --git a/packages/webapp/src/shared/components/routes/anonymousRoute/anonymousRoute.component.tsx b/packages/webapp/src/shared/components/routes/anonymousRoute/anonymousRoute.component.tsx index 8147ed67e..8bc5c6673 100644 --- a/packages/webapp/src/shared/components/routes/anonymousRoute/anonymousRoute.component.tsx +++ b/packages/webapp/src/shared/components/routes/anonymousRoute/anonymousRoute.component.tsx @@ -1,5 +1,5 @@ import { useGenerateLocalePath } from '@sb/webapp-core/hooks'; -import { Navigate, Outlet } from 'react-router-dom'; +import { Navigate, Outlet, useSearchParams } from 'react-router-dom'; import { RoutesConfig } from '../../../../app/config/routes'; import { useAuth } from '../../../hooks'; @@ -21,6 +21,8 @@ import { useAuth } from '../../../hooks'; export const AnonymousRoute = () => { const generateLocalePath = useGenerateLocalePath(); const { isLoggedIn } = useAuth(); + const [search] = useSearchParams(); + const redirect = search.get('redirect'); - return isLoggedIn ? : ; + return isLoggedIn ? : ; }; diff --git a/packages/webapp/src/shared/components/routes/authRoute/authRoute.component.tsx b/packages/webapp/src/shared/components/routes/authRoute/authRoute.component.tsx index cab0cd282..f2ebcd16d 100644 --- a/packages/webapp/src/shared/components/routes/authRoute/authRoute.component.tsx +++ b/packages/webapp/src/shared/components/routes/authRoute/authRoute.component.tsx @@ -4,6 +4,7 @@ import { Navigate, Outlet } from 'react-router-dom'; import { RoutesConfig } from '../../../../app/config/routes'; import { Role } from '../../../../modules/auth/auth.types'; import { useAuth, useRoleAccessCheck } from '../../../hooks'; +import { useGenerateRedirectSearchParams } from './authRoute.hook'; export type AuthRouteProps = { allowedRoles?: Role | Role[]; @@ -28,9 +29,21 @@ export type AuthRouteProps = { export const AuthRoute = ({ allowedRoles = [Role.ADMIN, Role.USER] }: AuthRouteProps) => { const { isLoggedIn } = useAuth(); const { isAllowed } = useRoleAccessCheck(allowedRoles); + + const generateRedirectSearchParams = useGenerateRedirectSearchParams(); const generateLocalePath = useGenerateLocalePath(); - const fallbackUrl = isLoggedIn ? generateLocalePath(RoutesConfig.notFound) : generateLocalePath(RoutesConfig.login); - if (!isAllowed) return ; + if (!isAllowed) { + if (isLoggedIn) return ; + + return ( + + ); + } return ; }; diff --git a/packages/webapp/src/shared/components/routes/authRoute/authRoute.hook.tsx b/packages/webapp/src/shared/components/routes/authRoute/authRoute.hook.tsx new file mode 100644 index 000000000..c20b79ad0 --- /dev/null +++ b/packages/webapp/src/shared/components/routes/authRoute/authRoute.hook.tsx @@ -0,0 +1,17 @@ +import { useGenerateLocalePath, useLocale } from '@sb/webapp-core/hooks'; +import { createSearchParams, useLocation } from 'react-router-dom'; + +export const useGenerateRedirectSearchParams = () => { + const generateLocalePath = useGenerateLocalePath(); + const locale = useLocale(); + const { pathname } = useLocation(); + + const generateRedirectSearchParams = () => { + const re = new RegExp(`/${locale}/?`); + const pathnameWithoutLocale = pathname.replace(re, ''); + const redirect = generateLocalePath(pathnameWithoutLocale); + return pathnameWithoutLocale ? createSearchParams({ redirect }).toString() : undefined; + }; + + return generateRedirectSearchParams; +};