mirror of
https://github.com/logto-io/logto.git
synced 2025-02-10 21:58:23 -05:00
refactor(ui): remove /404 path (#709)
* fix(ui): fix ci fail * fix(ui): fix redundent code fix redundent code * refactor(ui): render not found contend directly instead of jump to 404 render not found contend directly instead of jump to 404 * fix(ui): fix infinit toast bug fix inifinit toast bug * refactor(ui): show error toast show error toast * fix(ui): cr fix cr fix
This commit is contained in:
parent
38c453908c
commit
166fb709d3
9 changed files with 83 additions and 82 deletions
|
@ -54,7 +54,6 @@ const App = () => {
|
|||
<Route path="/callback/:connector" element={<Callback />} />
|
||||
<Route path="/social-register/:connector" element={<SocialRegister />} />
|
||||
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
|
||||
<Route path="/404" element={<NotFound />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Routes, Route, MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
|
||||
import Passcode from '.';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
|
@ -12,20 +13,8 @@ jest.mock('react-router-dom', () => ({
|
|||
}));
|
||||
|
||||
describe('Passcode Page', () => {
|
||||
it('render with invalid method should lead to 404 page', () => {
|
||||
const { queryByText } = render(
|
||||
<MemoryRouter initialEntries={['/sign-in/username/passcode-validation']}>
|
||||
<Routes>
|
||||
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(queryByText('action.enter_passcode')).toBeNull();
|
||||
});
|
||||
|
||||
it('render properly', () => {
|
||||
const { queryByText } = render(
|
||||
const { queryByText } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/sign-in/email/passcode-validation']}>
|
||||
<Routes>
|
||||
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
|
||||
|
@ -36,4 +25,30 @@ describe('Passcode Page', () => {
|
|||
expect(queryByText('action.enter_passcode')).not.toBeNull();
|
||||
expect(queryByText('description.enter_passcode')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('render with invalid method', () => {
|
||||
const { queryByText } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/sign-in/username/passcode-validation']}>
|
||||
<Routes>
|
||||
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(queryByText('action.enter_passcode')).toBeNull();
|
||||
expect(queryByText('description.not_found')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('render with invalid type', () => {
|
||||
const { queryByText } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/social-register/email/passcode-validation']}>
|
||||
<Routes>
|
||||
<Route path="/:type/:method/passcode-validation" element={<Passcode />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(queryByText('action.enter_passcode')).toBeNull();
|
||||
expect(queryByText('description.not_found')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { Nullable } from '@silverhand/essentials';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
|
||||
import NavBar from '@/components/NavBar';
|
||||
import PasscodeValidation from '@/containers/PasscodeValidation';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
@ -14,40 +16,30 @@ type Parameters = {
|
|||
method: string;
|
||||
};
|
||||
|
||||
type StateType = Nullable<{
|
||||
email?: string;
|
||||
sms?: string;
|
||||
}>;
|
||||
type StateType = Nullable<Record<string, string>>;
|
||||
|
||||
const Passcode = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const navigate = useNavigate();
|
||||
const { method, type } = useParams<Parameters>();
|
||||
const state = useLocation().state as StateType;
|
||||
const invalidSignInMethod = type !== 'sign-in' && type !== 'register';
|
||||
const invalidType = type !== 'sign-in' && type !== 'register';
|
||||
const invalidMethod = method !== 'email' && method !== 'sms';
|
||||
const { setToast } = useContext(PageContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (invalidSignInMethod || invalidMethod) {
|
||||
navigate('/404', { replace: true });
|
||||
|
||||
return;
|
||||
if (method && !state?.[method]) {
|
||||
setToast(t(method === 'email' ? 'error.invalid_email' : 'error.invalid_phone'));
|
||||
}
|
||||
}, [method, setToast, state, t]);
|
||||
|
||||
// Navigate to the back if no method value found
|
||||
if (!state?.[method]) {
|
||||
navigate(-1);
|
||||
}
|
||||
}, [invalidMethod, invalidSignInMethod, method, navigate, state]);
|
||||
|
||||
if (invalidSignInMethod || invalidMethod) {
|
||||
return null;
|
||||
if (invalidType || invalidMethod) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
const target = state?.[method];
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -40,4 +40,16 @@ describe('<Register />', () => {
|
|||
expect(queryByText('action.create_account')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('renders non-recognized method', async () => {
|
||||
const { queryByText } = render(
|
||||
<MemoryRouter initialEntries={['/register/test']}>
|
||||
<Routes>
|
||||
<Route path="/register/:method" element={<Register />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(queryByText('action.create_account')).toBeNull();
|
||||
expect(queryByText('description.not_found')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useMemo, useEffect } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import NavBar from '@/components/NavBar';
|
||||
import CreateAccount from '@/containers/CreateAccount';
|
||||
import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -14,15 +15,8 @@ type Parameters = {
|
|||
|
||||
const Register = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const navigate = useNavigate();
|
||||
const { method = 'username' } = useParams<Parameters>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!['email', 'sms', 'username'].includes(method)) {
|
||||
navigate('/404', { replace: true });
|
||||
}
|
||||
}, [method, navigate]);
|
||||
|
||||
const registerForm = useMemo(() => {
|
||||
if (method === 'sms') {
|
||||
return <PhonePasswordless type="register" />;
|
||||
|
@ -35,6 +29,10 @@ const Register = () => {
|
|||
return <CreateAccount />;
|
||||
}, [method]);
|
||||
|
||||
if (!['email', 'sms', 'username'].includes(method)) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<NavBar />
|
||||
|
|
|
@ -39,4 +39,16 @@ describe('<SecondarySignIn />', () => {
|
|||
expect(queryByText('action.sign_in')).not.toBeNull();
|
||||
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('render un-recognized method', async () => {
|
||||
const { queryByText } = render(
|
||||
<MemoryRouter initialEntries={['/sign-in/test']}>
|
||||
<Routes>
|
||||
<Route path="/sign-in/:method" element={<SecondarySignIn />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(queryByText('action.sign_in')).toBeNull();
|
||||
expect(queryByText('description.not_found')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useMemo, useEffect } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import NavBar from '@/components/NavBar';
|
||||
import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless';
|
||||
import UsernameSignin from '@/containers/UsernameSignin';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -14,15 +15,8 @@ type Props = {
|
|||
|
||||
const SecondarySignIn = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const navigate = useNavigate();
|
||||
const { method = 'username' } = useParams<Props>();
|
||||
|
||||
useEffect(() => {
|
||||
if (method !== 'email' && method !== 'sms' && method !== 'username') {
|
||||
navigate('/404', { replace: true });
|
||||
}
|
||||
}, [method, navigate]);
|
||||
|
||||
const signInForm = useMemo(() => {
|
||||
if (method === 'sms') {
|
||||
return <PhonePasswordless type="sign-in" />;
|
||||
|
@ -35,6 +29,10 @@ const SecondarySignIn = () => {
|
|||
return <UsernameSignin />;
|
||||
}, [method]);
|
||||
|
||||
if (!['email', 'sms', 'username'].includes(method)) {
|
||||
return <NotFound />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<NavBar />
|
||||
|
|
|
@ -4,26 +4,8 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|||
|
||||
import SocialRegister from '.';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
|
||||
describe('SocialRegister', () => {
|
||||
it('render null and redirect if no connector found', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/social-register']}>
|
||||
<Routes>
|
||||
<Route path="/social-register" element={<SocialRegister />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(mockNavigate).toBeCalledWith('/404');
|
||||
});
|
||||
|
||||
it('render with connection', () => {
|
||||
it('render', () => {
|
||||
const { queryByText } = render(
|
||||
<MemoryRouter initialEntries={['/social-register/github']}>
|
||||
<Routes>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import NavBar from '@/components/NavBar';
|
||||
import SocialCreateAccount from '@/containers/SocialCreateAccount';
|
||||
|
@ -14,13 +14,6 @@ type Parameters = {
|
|||
const SocialRegister = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||
const { connector } = useParams<Parameters>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!connector) {
|
||||
navigate('/404');
|
||||
}
|
||||
}, [connector, navigate]);
|
||||
|
||||
if (!connector) {
|
||||
return null;
|
||||
|
|
Loading…
Add table
Reference in a new issue