mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
feat(ui): add EmailSignIn container (#2308)
This commit is contained in:
parent
d60e1e8267
commit
96ade39498
19 changed files with 450 additions and 32 deletions
|
@ -17,6 +17,7 @@ import ResetPassword from './pages/ResetPassword';
|
|||
import SecondaryRegister from './pages/SecondaryRegister';
|
||||
import SecondarySignIn from './pages/SecondarySignIn';
|
||||
import SignIn from './pages/SignIn';
|
||||
import SignInPassword from './pages/SignInPassword';
|
||||
import SocialLanding from './pages/SocialLanding';
|
||||
import SocialRegister from './pages/SocialRegister';
|
||||
import SocialSignIn from './pages/SocialSignInCallback';
|
||||
|
@ -66,6 +67,7 @@ const App = () => {
|
|||
<Route path="/sign-in" element={<SignIn />} />
|
||||
<Route path="/sign-in/social/:connector" element={<SocialSignIn />} />
|
||||
<Route path="/sign-in/:method" element={<SecondarySignIn />} />
|
||||
<Route path="/sign-in/:method/password" element={<SignInPassword />} />
|
||||
|
||||
{/* register */}
|
||||
<Route path="/register" element={<Register />} />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
import type { TFuncKey } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
|
@ -20,6 +21,7 @@ type Props = {
|
|||
hasTerms?: boolean;
|
||||
hasSwitch?: boolean;
|
||||
errorMessage?: string;
|
||||
submitButtonText?: TFuncKey;
|
||||
clearErrorMessage?: () => void;
|
||||
onSubmit: (email: string) => Promise<void>;
|
||||
};
|
||||
|
@ -35,8 +37,9 @@ const EmailForm = ({
|
|||
hasTerms = true,
|
||||
hasSwitch = false,
|
||||
errorMessage,
|
||||
clearErrorMessage,
|
||||
className,
|
||||
submitButtonText = 'action.continue',
|
||||
clearErrorMessage,
|
||||
onSubmit,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -86,7 +89,7 @@ const EmailForm = ({
|
|||
{errorMessage && <ErrorMessage className={styles.formErrors}>{errorMessage}</ErrorMessage>}
|
||||
{hasSwitch && <PasswordlessSwitch target="sms" className={styles.switch} />}
|
||||
{hasTerms && <TermsOfUse className={styles.terms} />}
|
||||
<Button title="action.continue" onClick={async () => onSubmitHandler()} />
|
||||
<Button title={submitButtonText} onClick={async () => onSubmitHandler()} />
|
||||
|
||||
<input hidden type="submit" />
|
||||
</form>
|
||||
|
|
49
packages/ui/src/containers/EmailForm/EmailRegister.test.tsx
Normal file
49
packages/ui/src/containers/EmailForm/EmailRegister.test.tsx
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { sendRegisterEmailPasscode } from '@/apis/register';
|
||||
|
||||
import EmailRegister from './EmailRegister';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('@/apis/register', () => ({
|
||||
sendRegisterEmailPasscode: jest.fn(() => ({ success: true })),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
describe('EmailRegister', () => {
|
||||
const email = 'foo@logto.io';
|
||||
|
||||
test('register form submit', async () => {
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailRegister />
|
||||
</MemoryRouter>
|
||||
);
|
||||
const emailInput = container.querySelector('input[name="email"]');
|
||||
|
||||
if (emailInput) {
|
||||
fireEvent.change(emailInput, { target: { value: email } });
|
||||
}
|
||||
|
||||
const submitButton = getByText('action.create_account');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendRegisterEmailPasscode).toBeCalledWith(email);
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
{ pathname: '/register/email/passcode-validation', search: '' },
|
||||
{ state: { email } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -14,6 +14,7 @@ const EmailRegister = (props: Props) => {
|
|||
<EmailForm
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
submitButtonText="action.create_account"
|
||||
errorMessage={errorMessage}
|
||||
clearErrorMessage={clearErrorMessage}
|
||||
/>
|
||||
|
|
165
packages/ui/src/containers/EmailForm/EmailSignIn.test.tsx
Normal file
165
packages/ui/src/containers/EmailForm/EmailSignIn.test.tsx
Normal file
|
@ -0,0 +1,165 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { fireEvent, waitFor, act } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import { sendSignInEmailPasscode } from '@/apis/sign-in';
|
||||
|
||||
import EmailSignIn from './EmailSignIn';
|
||||
|
||||
const mockedNavigate = jest.fn();
|
||||
|
||||
jest.mock('@/apis/sign-in', () => ({
|
||||
sendSignInEmailPasscode: jest.fn(() => ({ success: true })),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedNavigate,
|
||||
}));
|
||||
|
||||
describe('EmailSignIn', () => {
|
||||
const email = 'foo@logto.io';
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('EmailSignIn form with password as primary method', async () => {
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailSignIn
|
||||
signInMethod={{
|
||||
identifier: SignInIdentifier.Email,
|
||||
password: true,
|
||||
verificationCode: true,
|
||||
isPasswordPrimary: true,
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
const emailInput = container.querySelector('input[name="email"]');
|
||||
|
||||
if (emailInput) {
|
||||
fireEvent.change(emailInput, { target: { value: email } });
|
||||
}
|
||||
|
||||
const submitButton = getByText('action.sign_in');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendSignInEmailPasscode).not.toBeCalled();
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
{ pathname: '/sign-in/email/password', search: '' },
|
||||
{ state: { email } }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('EmailSignIn form with password true but not primary verification code false', async () => {
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailSignIn
|
||||
signInMethod={{
|
||||
identifier: SignInIdentifier.Email,
|
||||
password: true,
|
||||
verificationCode: false,
|
||||
isPasswordPrimary: true,
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
const emailInput = container.querySelector('input[name="email"]');
|
||||
|
||||
if (emailInput) {
|
||||
fireEvent.change(emailInput, { target: { value: email } });
|
||||
}
|
||||
|
||||
const submitButton = getByText('action.sign_in');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendSignInEmailPasscode).not.toBeCalled();
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
{ pathname: '/sign-in/email/password', search: '' },
|
||||
{ state: { email } }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('EmailSignIn form with password true but not primary verification code true', async () => {
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailSignIn
|
||||
signInMethod={{
|
||||
identifier: SignInIdentifier.Email,
|
||||
password: true,
|
||||
verificationCode: true,
|
||||
isPasswordPrimary: false,
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const emailInput = container.querySelector('input[name="email"]');
|
||||
|
||||
if (emailInput) {
|
||||
fireEvent.change(emailInput, { target: { value: email } });
|
||||
}
|
||||
|
||||
const submitButton = getByText('action.sign_in');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendSignInEmailPasscode).toBeCalledWith(email);
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
{ pathname: '/sign-in/email/passcode-validation', search: '' },
|
||||
{ state: { email } }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('EmailSignIn form with password false but primary verification code true', async () => {
|
||||
const { container, getByText } = renderWithPageContext(
|
||||
<MemoryRouter>
|
||||
<EmailSignIn
|
||||
signInMethod={{
|
||||
identifier: SignInIdentifier.Email,
|
||||
password: false,
|
||||
verificationCode: true,
|
||||
isPasswordPrimary: true,
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const emailInput = container.querySelector('input[name="email"]');
|
||||
|
||||
if (emailInput) {
|
||||
fireEvent.change(emailInput, { target: { value: email } });
|
||||
}
|
||||
|
||||
const submitButton = getByText('action.sign_in');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(sendSignInEmailPasscode).toBeCalledWith(email);
|
||||
expect(mockedNavigate).toBeCalledWith(
|
||||
{ pathname: '/sign-in/email/passcode-validation', search: '' },
|
||||
{ state: { email } }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
26
packages/ui/src/containers/EmailForm/EmailSignIn.tsx
Normal file
26
packages/ui/src/containers/EmailForm/EmailSignIn.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import EmailForm from './EmailForm';
|
||||
import type { MethodProps } from './use-email-sign-in';
|
||||
import useEmailSignIn from './use-email-sign-in';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
// eslint-disable-next-line react/boolean-prop-naming
|
||||
autoFocus?: boolean;
|
||||
signInMethod: MethodProps;
|
||||
};
|
||||
|
||||
const EmailSignIn = ({ signInMethod, ...props }: Props) => {
|
||||
const { onSubmit, errorMessage, clearErrorMessage } = useEmailSignIn(signInMethod);
|
||||
|
||||
return (
|
||||
<EmailForm
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
submitButtonText="action.sign_in"
|
||||
errorMessage={errorMessage}
|
||||
clearErrorMessage={clearErrorMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmailSignIn;
|
89
packages/ui/src/containers/EmailForm/use-email-sign-in.ts
Normal file
89
packages/ui/src/containers/EmailForm/use-email-sign-in.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import type { SignIn } from '@logto/schemas';
|
||||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { useState, useMemo, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { sendSignInEmailPasscode } from '@/apis/sign-in';
|
||||
import type { ErrorHandlers } from '@/hooks/use-api';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import type { ArrayElement } from '@/types';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
export type MethodProps = ArrayElement<SignIn['methods']>;
|
||||
|
||||
const useEmailSignIn = ({ password, isPasswordPrimary, verificationCode }: MethodProps) => {
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const errorHandlers: ErrorHandlers = useMemo(
|
||||
() => ({
|
||||
'guard.invalid_input': () => {
|
||||
setErrorMessage('invalid_email');
|
||||
},
|
||||
}),
|
||||
[setErrorMessage]
|
||||
);
|
||||
|
||||
const clearErrorMessage = useCallback(() => {
|
||||
setErrorMessage('');
|
||||
}, []);
|
||||
|
||||
const { run: asyncSendSignInEmailPasscode } = useApi(sendSignInEmailPasscode, errorHandlers);
|
||||
|
||||
const navigateToPasswordPage = useCallback(
|
||||
(email: string) => {
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.signIn}/${SignInIdentifier.Email}/password`,
|
||||
search: location.search,
|
||||
},
|
||||
{ state: { email } }
|
||||
);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const sendPasscode = useCallback(
|
||||
async (email: string) => {
|
||||
const result = await asyncSendSignInEmailPasscode(email);
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
navigate(
|
||||
{
|
||||
pathname: `/${UserFlow.signIn}/${SignInIdentifier.Email}/passcode-validation`,
|
||||
search: location.search,
|
||||
},
|
||||
{ state: { email } }
|
||||
);
|
||||
},
|
||||
[asyncSendSignInEmailPasscode, navigate]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (email: string) => {
|
||||
// Email Password SignIn Flow
|
||||
if (password && (isPasswordPrimary || !verificationCode)) {
|
||||
navigateToPasswordPage(email);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Email Passwordless SignIn Flow
|
||||
if (verificationCode) {
|
||||
await sendPasscode(email);
|
||||
}
|
||||
},
|
||||
[isPasswordPrimary, navigateToPasswordPage, password, sendPasscode, verificationCode]
|
||||
);
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
clearErrorMessage,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEmailSignIn;
|
|
@ -9,7 +9,6 @@ import ConfirmModalProvider from '@/containers/ConfirmModalProvider';
|
|||
import EmailPassword from '.';
|
||||
|
||||
jest.mock('@/apis/sign-in', () => ({ signInWithEmailPassword: jest.fn(async () => 0) }));
|
||||
// Terms Iframe Modal only shown on mobile device
|
||||
jest.mock('react-device-detect', () => ({
|
||||
isMobile: true,
|
||||
}));
|
||||
|
|
|
@ -55,8 +55,6 @@ const EmailPassword = ({ className, autoFocus }: Props) => {
|
|||
async (event?: React.FormEvent<HTMLFormElement>) => {
|
||||
event?.preventDefault();
|
||||
|
||||
setErrorMessage(undefined);
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import classNames from 'classnames';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import type { TFuncKey } from 'react-i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
|
@ -20,6 +21,7 @@ type Props = {
|
|||
hasTerms?: boolean;
|
||||
hasSwitch?: boolean;
|
||||
errorMessage?: string;
|
||||
submitButtonText?: TFuncKey;
|
||||
clearErrorMessage?: () => void;
|
||||
onSubmit: (phone: string) => Promise<void>;
|
||||
};
|
||||
|
@ -36,6 +38,7 @@ const PhoneForm = ({
|
|||
hasSwitch = false,
|
||||
className,
|
||||
errorMessage,
|
||||
submitButtonText = 'action.continue',
|
||||
clearErrorMessage,
|
||||
onSubmit,
|
||||
}: Props) => {
|
||||
|
@ -105,7 +108,7 @@ const PhoneForm = ({
|
|||
|
||||
{hasTerms && <TermsOfUse className={styles.terms} />}
|
||||
|
||||
<Button title="action.continue" onClick={async () => onSubmitHandler()} />
|
||||
<Button title={submitButtonText} onClick={async () => onSubmitHandler()} />
|
||||
|
||||
<input hidden type="submit" />
|
||||
</form>
|
||||
|
|
|
@ -14,6 +14,7 @@ const SmsRegister = (props: Props) => {
|
|||
<PhoneForm
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
submitButtonText="action.create_account"
|
||||
errorMessage={errorMessage}
|
||||
clearErrorMessage={clearErrorMessage}
|
||||
/>
|
||||
|
|
|
@ -41,7 +41,7 @@ describe('<Register />', () => {
|
|||
</SettingsProvider>
|
||||
);
|
||||
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
||||
expect(queryByText('action.continue')).not.toBeNull();
|
||||
expect(queryByText('action.create_account')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('renders with sms passwordless as primary', async () => {
|
||||
|
@ -58,7 +58,7 @@ describe('<Register />', () => {
|
|||
</SettingsProvider>
|
||||
);
|
||||
expect(container.querySelector('input[name="phone"]')).not.toBeNull();
|
||||
expect(queryByText('action.continue')).not.toBeNull();
|
||||
expect(queryByText('action.create_account')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('render with email and sms passwordless', async () => {
|
||||
|
|
|
@ -13,7 +13,7 @@ jest.mock('i18next', () => ({
|
|||
|
||||
describe('<SecondaryRegister />', () => {
|
||||
test('renders phone', async () => {
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
const { queryAllByText, container } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/register/sms']}>
|
||||
<Routes>
|
||||
<Route
|
||||
|
@ -35,12 +35,12 @@ describe('<SecondaryRegister />', () => {
|
|||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(queryByText('action.create_account')).not.toBeNull();
|
||||
expect(queryAllByText('action.create_account')).toHaveLength(2);
|
||||
expect(container.querySelector('input[name="phone"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('renders email', async () => {
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
const { queryAllByText, container } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/register/email']}>
|
||||
<Routes>
|
||||
<Route
|
||||
|
@ -62,7 +62,7 @@ describe('<SecondaryRegister />', () => {
|
|||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(queryByText('action.create_account')).not.toBeNull();
|
||||
expect(queryAllByText('action.create_account')).toHaveLength(2);
|
||||
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { render } from '@testing-library/react';
|
||||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { Routes, Route, MemoryRouter } from 'react-router-dom';
|
||||
|
||||
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
|
||||
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
|
||||
import { mockSignInExperienceSettings } from '@/__mocks__/logto';
|
||||
import SecondarySignIn from '@/pages/SecondarySignIn';
|
||||
|
||||
jest.mock('@/apis/register', () => ({ register: jest.fn(async () => 0) }));
|
||||
|
@ -10,19 +13,35 @@ jest.mock('i18next', () => ({
|
|||
|
||||
describe('<SecondarySignIn />', () => {
|
||||
test('renders without exploding', async () => {
|
||||
const { queryAllByText } = render(
|
||||
const { queryAllByText } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/sign-in/username']}>
|
||||
<SecondarySignIn />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/sign-in/:method"
|
||||
element={
|
||||
<SettingsProvider>
|
||||
<SecondarySignIn />
|
||||
</SettingsProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(queryAllByText('action.sign_in')).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('renders phone', async () => {
|
||||
const { queryByText, container } = render(
|
||||
const { queryByText, container } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/sign-in/sms']}>
|
||||
<Routes>
|
||||
<Route path="/sign-in/:method" element={<SecondarySignIn />} />
|
||||
<Route
|
||||
path="/sign-in/:method"
|
||||
element={
|
||||
<SettingsProvider>
|
||||
<SecondarySignIn />
|
||||
</SettingsProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
@ -31,22 +50,64 @@ describe('<SecondarySignIn />', () => {
|
|||
});
|
||||
|
||||
test('renders email', async () => {
|
||||
const { queryByText, container } = render(
|
||||
const { queryAllByText, container } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/sign-in/email']}>
|
||||
<Routes>
|
||||
<Route path="/sign-in/:method" element={<SecondarySignIn />} />
|
||||
<Route
|
||||
path="/sign-in/:method"
|
||||
element={
|
||||
<SettingsProvider>
|
||||
<SecondarySignIn />
|
||||
</SettingsProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(queryByText('action.sign_in')).not.toBeNull();
|
||||
expect(queryAllByText('action.sign_in')).toHaveLength(2);
|
||||
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('render un-recognized method', async () => {
|
||||
const { queryByText } = render(
|
||||
const { queryByText } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/sign-in/test']}>
|
||||
<Routes>
|
||||
<Route path="/sign-in/:method" element={<SecondarySignIn />} />
|
||||
<Route
|
||||
path="/sign-in/:method"
|
||||
element={
|
||||
<SettingsProvider>
|
||||
<SecondarySignIn />
|
||||
</SettingsProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(queryByText('action.sign_in')).toBeNull();
|
||||
expect(queryByText('description.not_found')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('render un-supported method', async () => {
|
||||
const { queryByText } = renderWithPageContext(
|
||||
<MemoryRouter initialEntries={['/sign-in/email']}>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/sign-in/:method"
|
||||
element={
|
||||
<SettingsProvider
|
||||
settings={{
|
||||
...mockSignInExperienceSettings,
|
||||
signIn: {
|
||||
methods: mockSignInExperienceSettings.signIn.methods.filter(
|
||||
({ identifier }) => identifier !== SignInIdentifier.Email
|
||||
),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SecondarySignIn />
|
||||
</SettingsProvider>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
|
|
@ -2,8 +2,10 @@ import { useMemo } from 'react';
|
|||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import SecondaryPageWrapper from '@/components/SecondaryPageWrapper';
|
||||
import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless';
|
||||
import EmailSignIn from '@/containers/EmailForm/EmailSignIn';
|
||||
import { PhonePasswordless } from '@/containers/Passwordless';
|
||||
import UsernameSignIn from '@/containers/UsernameSignIn';
|
||||
import { useSieMethods } from '@/hooks/use-sie';
|
||||
import ErrorPage from '@/pages/ErrorPage';
|
||||
import { UserFlow } from '@/types';
|
||||
|
||||
|
@ -13,6 +15,7 @@ type Props = {
|
|||
|
||||
const SecondarySignIn = () => {
|
||||
const { method = 'username' } = useParams<Props>();
|
||||
const { signInMethods } = useSieMethods();
|
||||
|
||||
const signInForm = useMemo(() => {
|
||||
if (method === 'sms') {
|
||||
|
@ -21,18 +24,24 @@ const SecondarySignIn = () => {
|
|||
}
|
||||
|
||||
if (method === 'email') {
|
||||
const signInMethod = signInMethods.find(({ identifier }) => identifier === method);
|
||||
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
return <EmailPasswordless autoFocus type={UserFlow.signIn} />;
|
||||
return signInMethod && <EmailSignIn autoFocus signInMethod={signInMethod} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
return <UsernameSignIn autoFocus />;
|
||||
}, [method]);
|
||||
}, [method, signInMethods]);
|
||||
|
||||
if (!['email', 'sms', 'username'].includes(method)) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
if (!signInMethods.some(({ identifier }) => identifier === method)) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
return <SecondaryPageWrapper title="action.sign_in">{signInForm}</SecondaryPageWrapper>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import type { SignIn as SignInType, ConnectorMetadata } from '@logto/schemas';
|
||||
|
||||
import EmailSignIn from '@/containers/EmailForm/EmailSignIn';
|
||||
import EmailPassword from '@/containers/EmailPassword';
|
||||
import { EmailPasswordless, PhonePasswordless } from '@/containers/Passwordless';
|
||||
import { PhonePasswordless } from '@/containers/Passwordless';
|
||||
import PhonePassword from '@/containers/PhonePassword';
|
||||
import SocialSignIn from '@/containers/SocialSignIn';
|
||||
import UsernameSignIn from '@/containers/UsernameSignIn';
|
||||
|
@ -17,15 +19,15 @@ type Props = {
|
|||
|
||||
const Main = ({ signInMethod, socialConnectors }: Props) => {
|
||||
switch (signInMethod?.identifier) {
|
||||
case 'email': {
|
||||
case SignInIdentifier.Email: {
|
||||
if (signInMethod.password && !signInMethod.verificationCode) {
|
||||
return <EmailPassword className={styles.main} />;
|
||||
}
|
||||
|
||||
return <EmailPasswordless type={UserFlow.signIn} className={styles.main} />;
|
||||
return <EmailSignIn signInMethod={signInMethod} className={styles.main} />;
|
||||
}
|
||||
|
||||
case 'sms': {
|
||||
case SignInIdentifier.Sms: {
|
||||
if (signInMethod.password && !signInMethod.verificationCode) {
|
||||
return <PhonePassword className={styles.main} />;
|
||||
}
|
||||
|
@ -33,7 +35,7 @@ const Main = ({ signInMethod, socialConnectors }: Props) => {
|
|||
return <PhonePasswordless type={UserFlow.signIn} className={styles.main} />;
|
||||
}
|
||||
|
||||
case 'username': {
|
||||
case SignInIdentifier.Username: {
|
||||
return <UsernameSignIn className={styles.main} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ describe('<SignIn />', () => {
|
|||
</SettingsProvider>
|
||||
);
|
||||
expect(container.querySelector('input[name="email"]')).not.toBeNull();
|
||||
expect(queryByText('action.continue')).not.toBeNull();
|
||||
expect(queryByText('action.sign_in')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('render with email password as primary', async () => {
|
||||
|
|
|
@ -22,7 +22,12 @@ const SignIn = () => {
|
|||
{
|
||||
// Other sign-in methods
|
||||
otherMethods.length > 0 && (
|
||||
<OtherMethodsLink methods={otherMethods} template="sign_in_with" flow={UserFlow.signIn} />
|
||||
<OtherMethodsLink
|
||||
methods={otherMethods}
|
||||
template="sign_in_with"
|
||||
flow={UserFlow.signIn}
|
||||
search={location.search}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
|
|
5
packages/ui/src/pages/SignInPassword/index.tsx
Normal file
5
packages/ui/src/pages/SignInPassword/index.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
const SignInPassword = () => {
|
||||
return <div>sign in password</div>;
|
||||
};
|
||||
|
||||
export default SignInPassword;
|
Loading…
Add table
Reference in a new issue