0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

refactor(ui): update global page context (#599)

* refactor(ui): update global page context

add usePageContext hook

* fix(ui): cr fix
cr fix
This commit is contained in:
simeng-li 2022-04-21 14:08:16 +08:00 committed by GitHub
parent 2fa20363be
commit a04f472fe0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 163 additions and 156 deletions

View file

@ -1,8 +1,8 @@
import React, { useState, useMemo, useEffect } from 'react';
import React, { useEffect } from 'react';
import { Route, Routes, BrowserRouter, Navigate } from 'react-router-dom';
import AppContent from './components/AppContent';
import PageContext from './hooks/page-context';
import usePageContext from './hooks/use-page-context';
import initI18n from './i18n/init';
import Callback from './pages/Callback';
import Consent from './pages/Consent';
@ -10,20 +10,13 @@ import Passcode from './pages/Passcode';
import Register from './pages/Register';
import SecondarySignIn from './pages/SecondarySignIn';
import SignIn from './pages/SignIn';
import { SignInExperienceSettings } from './types';
import getSignInExperienceSettings from './utils/sign-in-experience';
import './scss/normalized.scss';
const App = () => {
const [loading, setLoading] = useState(false);
const [toast, setToast] = useState('');
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceSettings>();
const context = useMemo(
() => ({ toast, loading, experienceSettings, setLoading, setToast, setExperienceSettings }),
[experienceSettings, loading, toast]
);
const { context, Provider } = usePageContext();
const { experienceSettings, setLoading, setExperienceSettings } = context;
useEffect(() => {
(async () => {
@ -37,14 +30,14 @@ const App = () => {
setLoading(false);
})();
}, []);
}, [setExperienceSettings, setLoading]);
if (!experienceSettings) {
return null;
}
return (
<PageContext.Provider value={context}>
<Provider value={context}>
<AppContent>
<BrowserRouter>
<Routes>
@ -61,7 +54,7 @@ const App = () => {
</Routes>
</BrowserRouter>
</AppContent>
</PageContext.Provider>
</Provider>
);
};

View file

@ -1,19 +0,0 @@
import React, { useState, useMemo } from 'react';
import PageContext from '@/hooks/page-context';
import { SignInExperienceSettings } from '@/types';
const ContextProvider = ({ children }: { children: React.ReactNode }) => {
const [loading, setLoading] = useState(false);
const [toast, setToast] = useState('');
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceSettings>();
const context = useMemo(
() => ({ loading, setLoading, toast, setToast, experienceSettings, setExperienceSettings }),
[experienceSettings, loading, toast]
);
return <PageContext.Provider value={context}>{children}</PageContext.Provider>;
};
export default ContextProvider;

View file

@ -0,0 +1,11 @@
import React from 'react';
import usePageContext from '@/hooks/use-page-context';
const ContextProvider = ({ children }: { children: React.ReactNode }) => {
const { context, Provider } = usePageContext();
return <Provider value={context}>{children}</Provider>;
};
export default ContextProvider;

View file

@ -0,0 +1,14 @@
import { render, Queries, queries, RenderOptions } from '@testing-library/react';
import React, { ReactElement } from 'react';
import ContextProvider from './ContextProvider';
const renderWithPageContext = <
Q extends Queries = typeof queries,
Container extends Element | DocumentFragment = HTMLElement
>(
ui: ReactElement,
options: RenderOptions<Q, Container> = {}
) => render<Q, Container>(<ContextProvider>{ui}</ContextProvider>, options);
export default renderWithPageContext;

View file

@ -42,7 +42,7 @@ $font-family-small: 'PingFang SC', 'SF Pro Text', 'Siyuan Heiti', 'Roboto';
--color-layer: linear-gradient(0deg, rgba(202, 190, 255, 14%), rgba(202, 190, 255, 14%)), linear-gradient(0deg, rgba(196, 199, 199, 2%), rgba(196, 199, 199, 2%)), #191c1d;
--color-error: #dd3730;
--color-toast: rgba(247, 248, 248, 80%);
--color-overlay: rgba(247, 248, 248, 80%);
--color-overlay: rgba(247, 248, 248, 16%);
--color-base: #191c1d;
}

View file

@ -3,7 +3,7 @@ import React, { ReactNode, useCallback, useContext } from 'react';
import LoadingLayer from '@/components/LoadingLayer';
import Toast from '@/components/Toast';
import PageContext from '@/hooks/page-context';
import { PageContext } from '@/hooks/use-page-context';
import useTheme from '@/hooks/use-theme';
import * as styles from './index.module.scss';

View file

@ -1,21 +1,16 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { register } from '@/apis/register';
import CreateAccount from '.';
jest.mock('@/apis/register', () => ({ register: jest.fn(async () => Promise.resolve()) }));
jest.mock('@/hooks/page-context', () =>
React.createContext({
loading: false,
setLoading: jest.fn(),
})
);
describe('<CreateAccount/>', () => {
test('default render', () => {
const { queryByText, container } = render(<CreateAccount />);
const { queryByText, container } = renderWithPageContext(<CreateAccount />);
expect(container.querySelector('input[name="username"]')).not.toBeNull();
expect(container.querySelector('input[name="password"]')).not.toBeNull();
expect(container.querySelector('input[name="confirm_password"]')).not.toBeNull();
@ -24,7 +19,7 @@ describe('<CreateAccount/>', () => {
});
test('username and password are required', () => {
const { queryAllByText, getByText } = render(<CreateAccount />);
const { queryAllByText, getByText } = renderWithPageContext(<CreateAccount />);
const submitButton = getByText('action.create');
fireEvent.click(submitButton);
@ -34,7 +29,7 @@ describe('<CreateAccount/>', () => {
});
test('username with initial numeric char should throw', () => {
const { queryByText, getByText, container } = render(<CreateAccount />);
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
const submitButton = getByText('action.create');
const usernameInput = container.querySelector('input[name="username"]');
@ -58,7 +53,7 @@ describe('<CreateAccount/>', () => {
});
test('username with special character should throw', () => {
const { queryByText, getByText, container } = render(<CreateAccount />);
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
const submitButton = getByText('action.create');
const usernameInput = container.querySelector('input[name="username"]');
@ -81,7 +76,7 @@ describe('<CreateAccount/>', () => {
});
test('password less than 6 chars should throw', () => {
const { queryByText, getByText, container } = render(<CreateAccount />);
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
const submitButton = getByText('action.create');
const passwordInput = container.querySelector('input[name="password"]');
@ -104,7 +99,7 @@ describe('<CreateAccount/>', () => {
});
test('password mismatch with confirmPassword should throw', () => {
const { queryByText, getByText, container } = render(<CreateAccount />);
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
const submitButton = getByText('action.create');
const passwordInput = container.querySelector('input[name="password"]');
const confirmPasswordInput = container.querySelector('input[name="confirm_password"]');
@ -137,7 +132,7 @@ describe('<CreateAccount/>', () => {
});
test('terms of use not checked should throw', () => {
const { queryByText, getByText, container } = render(<CreateAccount />);
const { queryByText, getByText, container } = renderWithPageContext(<CreateAccount />);
const submitButton = getByText('action.create');
const passwordInput = container.querySelector('input[name="password"]');
const confirmPasswordInput = container.querySelector('input[name="confirm_password"]');
@ -169,7 +164,7 @@ describe('<CreateAccount/>', () => {
});
test('submit form properly', async () => {
const { getByText, container } = render(<CreateAccount />);
const { getByText, container } = renderWithPageContext(<CreateAccount />);
const submitButton = getByText('action.create');
const passwordInput = container.querySelector('input[name="password"]');
const confirmPasswordInput = container.querySelector('input[name="confirm_password"]');

View file

@ -15,8 +15,8 @@ import { ErrorType } from '@/components/ErrorMessage';
import Input from '@/components/Input';
import PasswordInput from '@/components/Input/PasswordInput';
import TermsOfUse from '@/components/TermsOfUse';
import PageContext from '@/hooks/page-context';
import useApi from '@/hooks/use-api';
import { PageContext } from '@/hooks/use-page-context';
import * as styles from './index.module.scss';

View file

@ -10,7 +10,7 @@
width: 100%;
}
.field {
.inputField {
margin-bottom: _.unit(8);
&.withError {

View file

@ -1,6 +1,8 @@
import { render, act, fireEvent, waitFor } from '@testing-library/react';
import { act, fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import PasscodeValidation from '.';
jest.useFakeTimers();
@ -13,13 +15,6 @@ jest.mock('@/apis/utils', () => ({
getVerifyPasscodeApi: () => verifyPasscodeApi,
}));
jest.mock('@/hooks/page-context', () =>
React.createContext({
loading: false,
setLoading: jest.fn(),
})
);
describe('<PasscodeValidation />', () => {
const email = 'foo@logto.io';
@ -28,7 +23,7 @@ describe('<PasscodeValidation />', () => {
});
it('render counter', () => {
const { queryByText } = render(
const { queryByText } = renderWithPageContext(
<PasscodeValidation type="sign-in" method="email" target={email} />
);
@ -42,7 +37,7 @@ describe('<PasscodeValidation />', () => {
});
it('fire resend event', async () => {
const { getByText } = render(
const { getByText } = renderWithPageContext(
<PasscodeValidation type="sign-in" method="email" target={email} />
);
act(() => {
@ -58,7 +53,7 @@ describe('<PasscodeValidation />', () => {
});
it('fire validate passcode event', async () => {
const { container } = render(
const { container } = renderWithPageContext(
<PasscodeValidation type="sign-in" method="email" target={email} />
);
const inputs = container.querySelectorAll('input');

View file

@ -8,8 +8,8 @@ import { getSendPasscodeApi, getVerifyPasscodeApi } from '@/apis/utils';
import { ErrorType } from '@/components/ErrorMessage';
import Passcode, { defaultLength } from '@/components/Passcode';
import TextLink from '@/components/TextLink';
import PageContext from '@/hooks/page-context';
import useApi from '@/hooks/use-api';
import { PageContext } from '@/hooks/use-page-context';
import { UserFlow } from '@/types';
import * as styles from './index.module.scss';
@ -102,7 +102,7 @@ const PasscodeValidation = ({ type, method, className, target }: Props) => {
<form className={classNames(styles.form, className)}>
<Passcode
name="passcode"
className={classNames(styles.field, error && styles.withError)}
className={classNames(styles.inputField, error && styles.withError)}
value={code}
error={error}
onChange={setCode}

View file

@ -1,7 +1,8 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { sendRegisterEmailPasscode } from '@/apis/register';
import { sendSignInEmailPasscode } from '@/apis/sign-in';
@ -13,16 +14,10 @@ jest.mock('@/apis/sign-in', () => ({
jest.mock('@/apis/register', () => ({
sendRegisterEmailPasscode: jest.fn(async () => Promise.resolve()),
}));
jest.mock('@/hooks/page-context', () =>
React.createContext({
loading: false,
setLoading: jest.fn(),
})
);
describe('<EmailPasswordless/>', () => {
test('render', () => {
const { queryByText, container } = render(
const { queryByText, container } = renderWithPageContext(
<MemoryRouter>
<EmailPasswordless type="sign-in" />
</MemoryRouter>
@ -33,7 +28,7 @@ describe('<EmailPasswordless/>', () => {
});
test('required email with error message', () => {
const { queryByText, container, getByText } = render(
const { queryByText, container, getByText } = renderWithPageContext(
<MemoryRouter>
<EmailPasswordless type="sign-in" />
</MemoryRouter>
@ -56,7 +51,7 @@ describe('<EmailPasswordless/>', () => {
});
test('required terms of agreement with error message', () => {
const { queryByText, container, getByText } = render(
const { queryByText, container, getByText } = renderWithPageContext(
<MemoryRouter>
<EmailPasswordless type="sign-in" />
</MemoryRouter>
@ -73,7 +68,7 @@ describe('<EmailPasswordless/>', () => {
});
test('signin method properly', async () => {
const { container, getByText } = render(
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<EmailPasswordless type="sign-in" />
</MemoryRouter>
@ -96,7 +91,7 @@ describe('<EmailPasswordless/>', () => {
});
test('register method properly', async () => {
const { container, getByText } = render(
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<EmailPasswordless type="register" />
</MemoryRouter>

View file

@ -14,8 +14,8 @@ import Button from '@/components/Button';
import { ErrorType } from '@/components/ErrorMessage';
import Input from '@/components/Input';
import TermsOfUse from '@/components/TermsOfUse';
import PageContext from '@/hooks/page-context';
import useApi from '@/hooks/use-api';
import { PageContext } from '@/hooks/use-page-context';
import { UserFlow } from '@/types';
import * as styles from './index.module.scss';

View file

@ -1,7 +1,8 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { sendRegisterSmsPasscode } from '@/apis/register';
import { sendSignInSmsPasscode } from '@/apis/sign-in';
import { defaultCountryCallingCode } from '@/hooks/use-phone-number';
@ -14,18 +15,12 @@ jest.mock('@/apis/sign-in', () => ({
jest.mock('@/apis/register', () => ({
sendRegisterSmsPasscode: jest.fn(async () => Promise.resolve()),
}));
jest.mock('@/hooks/page-context', () =>
React.createContext({
loading: false,
setLoading: jest.fn(),
})
);
describe('<PhonePasswordless/>', () => {
const phoneNumber = '18888888888';
test('render', () => {
const { queryByText, container } = render(
const { queryByText, container } = renderWithPageContext(
<MemoryRouter>
<PhonePasswordless type="sign-in" />
</MemoryRouter>
@ -36,7 +31,7 @@ describe('<PhonePasswordless/>', () => {
});
test('required phone with error message', () => {
const { queryByText, container, getByText } = render(
const { queryByText, container, getByText } = renderWithPageContext(
<MemoryRouter>
<PhonePasswordless type="sign-in" />
</MemoryRouter>
@ -59,7 +54,7 @@ describe('<PhonePasswordless/>', () => {
});
test('required terms of agreement with error message', () => {
const { queryByText, container, getByText } = render(
const { queryByText, container, getByText } = renderWithPageContext(
<MemoryRouter>
<PhonePasswordless type="sign-in" />
</MemoryRouter>
@ -76,7 +71,7 @@ describe('<PhonePasswordless/>', () => {
});
test('signin method properly', async () => {
const { container, getByText } = render(
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<PhonePasswordless type="sign-in" />
</MemoryRouter>
@ -99,7 +94,7 @@ describe('<PhonePasswordless/>', () => {
});
test('register method properly', async () => {
const { container, getByText } = render(
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<PhonePasswordless type="register" />
</MemoryRouter>

View file

@ -14,8 +14,8 @@ import Button from '@/components/Button';
import { ErrorType } from '@/components/ErrorMessage';
import PhoneInput from '@/components/Input/PhoneInput';
import TermsOfUse from '@/components/TermsOfUse';
import PageContext from '@/hooks/page-context';
import useApi from '@/hooks/use-api';
import { PageContext } from '@/hooks/use-page-context';
import usePhoneNumber, { countryList } from '@/hooks/use-phone-number';
import { UserFlow } from '@/types';

View file

@ -2,7 +2,7 @@ import { render, fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import ContextProvider from '@/__mocks__/ContextProvider';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { socialConnectors } from '@/__mocks__/logto';
import * as socialSignInApi from '@/apis/social';
import { generateState, storeState } from '@/hooks/use-social';
@ -57,12 +57,10 @@ describe('SecondarySocialSignIn', () => {
it('invoke web social signIn', async () => {
const connectors = socialConnectors.slice(0, 1);
const { container } = render(
<ContextProvider>
<MemoryRouter>
<SecondarySocialSignIn connectors={connectors} />
</MemoryRouter>
</ContextProvider>
const { container } = renderWithPageContext(
<MemoryRouter>
<SecondarySocialSignIn connectors={connectors} />
</MemoryRouter>
);
const socialButton = container.querySelector('button');
@ -82,12 +80,10 @@ describe('SecondarySocialSignIn', () => {
/* eslint-enable @silverhand/fp/no-mutation */
const connectors = socialConnectors.slice(0, 1);
const { container } = render(
<ContextProvider>
<MemoryRouter>
<SecondarySocialSignIn connectors={connectors} />
</MemoryRouter>
</ContextProvider>
const { container } = renderWithPageContext(
<MemoryRouter>
<SecondarySocialSignIn connectors={connectors} />
</MemoryRouter>
);
const socialButton = container.querySelector('button');
@ -118,17 +114,15 @@ describe('SecondarySocialSignIn', () => {
});
/* eslint-enable @silverhand/fp/no-mutating-methods */
render(
<ContextProvider>
<MemoryRouter initialEntries={['/sign-in/callback/github']}>
<Routes>
<Route
path="/sign-in/callback/:connector"
element={<SecondarySocialSignIn connectors={connectors} />}
/>
</Routes>
</MemoryRouter>
</ContextProvider>
renderWithPageContext(
<MemoryRouter initialEntries={['/sign-in/callback/github']}>
<Routes>
<Route
path="/sign-in/callback/:connector"
element={<SecondarySocialSignIn connectors={connectors} />}
/>
</Routes>
</MemoryRouter>
);
await waitFor(() => {

View file

@ -1,21 +1,16 @@
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, waitFor } from '@testing-library/react';
import React from 'react';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import { signInBasic } from '@/apis/sign-in';
import UsernameSignin from '.';
jest.mock('@/apis/sign-in', () => ({ signInBasic: jest.fn(async () => Promise.resolve()) }));
jest.mock('@/hooks/page-context', () =>
React.createContext({
loading: false,
setLoading: jest.fn(),
})
);
describe('<UsernameSignin>', () => {
test('render', () => {
const { queryByText, container } = render(<UsernameSignin />);
const { queryByText, container } = renderWithPageContext(<UsernameSignin />);
expect(container.querySelector('input[name="username"]')).not.toBeNull();
expect(container.querySelector('input[name="password"]')).not.toBeNull();
expect(queryByText('action.sign_in')).not.toBeNull();
@ -23,7 +18,9 @@ describe('<UsernameSignin>', () => {
});
test('required inputs with error message', () => {
const { queryByText, queryAllByText, getByText, container } = render(<UsernameSignin />);
const { queryByText, queryAllByText, getByText, container } = renderWithPageContext(
<UsernameSignin />
);
const submitButton = getByText('action.sign_in');
fireEvent.click(submitButton);
@ -53,7 +50,7 @@ describe('<UsernameSignin>', () => {
});
test('submit form', async () => {
const { getByText, container } = render(<UsernameSignin />);
const { getByText, container } = renderWithPageContext(<UsernameSignin />);
const submitButton = getByText('action.sign_in');
const usernameInput = container.querySelector('input[name="username"]');

View file

@ -15,8 +15,8 @@ import { ErrorType } from '@/components/ErrorMessage';
import Input from '@/components/Input';
import PasswordInput from '@/components/Input/PasswordInput';
import TermsOfUse from '@/components/TermsOfUse';
import PageContext from '@/hooks/page-context';
import useApi from '@/hooks/use-api';
import { PageContext } from '@/hooks/use-page-context';
import * as styles from './index.module.scss';

View file

@ -1,27 +0,0 @@
import React from 'react';
import { SignInExperienceSettings } from '@/types';
type Context = {
toast: string;
loading: boolean;
setToast: (message: string) => void;
setLoading: (loading: boolean) => void;
experienceSettings: SignInExperienceSettings | undefined;
setExperienceSettings: (settings: SignInExperienceSettings) => void;
};
const NOOP = () => {
throw new Error('Context provider not found');
};
const PageContext = React.createContext<Context>({
toast: '',
loading: false,
setToast: NOOP,
setLoading: NOOP,
experienceSettings: undefined,
setExperienceSettings: NOOP,
});
export default PageContext;

View file

@ -2,7 +2,7 @@ import { RequestErrorBody } from '@logto/schemas';
import { HTTPError } from 'ky';
import { useState, useCallback, useContext } from 'react';
import PageContext from '@/hooks/page-context';
import { PageContext } from '@/hooks/use-page-context';
type UseApi<T extends any[], U> = {
result?: U;

View file

@ -0,0 +1,64 @@
import { useState, useMemo, createContext } from 'react';
import { SignInExperienceSettings } from '@/types';
type Context = {
toast: string;
loading: boolean;
termsAgreement: boolean;
showTermsModal: boolean;
experienceSettings: SignInExperienceSettings | undefined;
setToast: (message: string) => void;
setLoading: (loading: boolean) => void;
setTermsAgreement: (termsAgreement: boolean) => void;
setShowTermsModal: (showTermsModal: boolean) => void;
setExperienceSettings: (settings: SignInExperienceSettings) => void;
};
const noop = () => {
throw new Error('Context provider not found');
};
export const PageContext = createContext<Context>({
toast: '',
loading: false,
termsAgreement: false,
showTermsModal: false,
experienceSettings: undefined,
setToast: noop,
setLoading: noop,
setTermsAgreement: noop,
setShowTermsModal: noop,
setExperienceSettings: noop,
});
const usePageContext = () => {
const [loading, setLoading] = useState(false);
const [toast, setToast] = useState('');
const [experienceSettings, setExperienceSettings] = useState<SignInExperienceSettings>();
const [termsAgreement, setTermsAgreement] = useState(false);
const [showTermsModal, setShowTermsModal] = useState(false);
const context = useMemo(
() => ({
toast,
loading,
termsAgreement,
showTermsModal,
experienceSettings,
setLoading,
setToast,
setTermsAgreement,
setShowTermsModal,
setExperienceSettings,
}),
[experienceSettings, loading, showTermsModal, termsAgreement, toast]
);
return {
context,
Provider: PageContext.Provider,
};
};
export default usePageContext;

View file

@ -4,8 +4,8 @@ import { useParams } from 'react-router-dom';
import { invokeSocialSignIn, signInWithSocial } from '@/apis/social';
import { generateRandomString, parseQueryParameters } from '@/utils';
import PageContext from './page-context';
import useApi from './use-api';
import { PageContext } from './use-page-context';
/**
* Social Connector State Utility Methods

View file

@ -1,6 +1,6 @@
import { useState, useEffect, useContext } from 'react';
import PageContext from './page-context';
import { PageContext } from './use-page-context';
export type Theme = 'dark' | 'light';