0
Fork 0
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:
simeng-li 2022-05-05 10:57:54 +08:00 committed by GitHub
parent 38c453908c
commit 166fb709d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 83 additions and 82 deletions

View file

@ -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>

View file

@ -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();
});
});

View file

@ -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 (

View file

@ -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();
});
});

View file

@ -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 />

View file

@ -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();
});
});

View file

@ -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 />

View file

@ -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>

View file

@ -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;