0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

feat(ui): add passwordless switch (#1976)

add passwordless switch
This commit is contained in:
simeng-li 2022-09-23 10:33:00 +08:00 committed by GitHub
parent 9a89c1a200
commit ddb0e47950
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 447 additions and 189 deletions

View file

@ -7,7 +7,13 @@ import { appendPath } from '@/utils/url';
// Need To Align With UI
export const sessionNotFoundPath = '/unknown-session';
export const guardedPath = ['/sign-in', '/register', '/social-register'];
export const guardedPath = [
'/sign-in',
'/register',
'/social/register',
'/reset-password',
'/forgot-password',
];
export default function koaSpaSessionGuard<
StateT,

View file

@ -26,6 +26,7 @@ const translation = {
got_it: 'Got it',
sign_in_with: 'Sign in with {{name}}',
forgot_password: 'Forgot Password?',
switch_to: 'Switch to {{method}}',
},
description: {
email: 'email',

View file

@ -28,6 +28,7 @@ const translation = {
got_it: 'Compris',
sign_in_with: 'Connexion avec {{name}}',
forgot_password: 'Mot de passe oublié ?',
switch_to: 'Passer au {{method}}',
},
description: {
email: 'email',

View file

@ -28,6 +28,7 @@ const translation = {
got_it: '알겠습니다',
sign_in_with: '{{name}} 로그인',
forgot_password: '비밀번호를 잊어버리셨나요?',
switch_to: 'Switch to {{method}}', // TODO: untranslated
},
description: {
email: '이메일',

View file

@ -28,6 +28,7 @@ const translation = {
got_it: 'Entendi',
sign_in_with: 'Entrar com {{name}}',
forgot_password: 'Esqueceu a password?',
switch_to: 'Mudar para {{method}}',
},
description: {
email: 'email',

View file

@ -28,6 +28,7 @@ const translation = {
got_it: 'Anladım',
sign_in_with: '{{name}} ile giriş yap',
forgot_password: 'Şifremi Unuttum?',
switch_to: 'Switch to {{method}}', // TODO: not translated
},
description: {
email: 'e-posta adresi',

View file

@ -28,6 +28,7 @@ const translation = {
got_it: '知道了',
sign_in_with: '通过 {{name}} 登录',
forgot_password: '忘记密码?',
switch_to: '切换到{{method}}',
},
description: {
email: '邮箱',

View file

@ -11,7 +11,17 @@
margin-bottom: _.unit(4);
}
.formFields {
margin-bottom: _.unit(8);
}
.terms {
margin: _.unit(8) 0 _.unit(4);
margin-bottom: _.unit(4);
}
}
:global(body.desktop) {
.formFields {
margin-bottom: _.unit(2);
}
}

View file

@ -85,41 +85,44 @@ const CreateAccount = ({ className, autoFocus }: Props) => {
return (
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
<Input
autoFocus={autoFocus}
className={styles.inputField}
name="new-username"
placeholder={t('input.username')}
{...fieldRegister('username', usernameValidation)}
onClear={() => {
setFieldValue((state) => ({ ...state, username: '' }));
}}
/>
<Input
className={styles.inputField}
name="new-password"
type="password"
autoComplete="new-password"
placeholder={t('input.password')}
{...fieldRegister('password', passwordValidation)}
onClear={() => {
setFieldValue((state) => ({ ...state, password: '' }));
}}
/>
<Input
className={styles.inputField}
name="confirm-new-password"
type="password"
autoComplete="new-password"
placeholder={t('input.confirm_password')}
{...fieldRegister('confirmPassword', (confirmPassword) =>
confirmPasswordValidation(fieldValue.password, confirmPassword)
)}
errorStyling={false}
onClear={() => {
setFieldValue((state) => ({ ...state, confirmPassword: '' }));
}}
/>
<div className={styles.formFields}>
<Input
autoFocus={autoFocus}
className={styles.inputField}
name="new-username"
placeholder={t('input.username')}
{...fieldRegister('username', usernameValidation)}
onClear={() => {
setFieldValue((state) => ({ ...state, username: '' }));
}}
/>
<Input
className={styles.inputField}
name="new-password"
type="password"
autoComplete="new-password"
placeholder={t('input.password')}
{...fieldRegister('password', passwordValidation)}
onClear={() => {
setFieldValue((state) => ({ ...state, password: '' }));
}}
/>
<Input
className={styles.inputField}
name="confirm-new-password"
type="password"
autoComplete="new-password"
placeholder={t('input.confirm_password')}
{...fieldRegister('confirmPassword', (confirmPassword) =>
confirmPasswordValidation(fieldValue.password, confirmPassword)
)}
errorStyling={false}
onClear={() => {
setFieldValue((state) => ({ ...state, confirmPassword: '' }));
}}
/>
</div>
<TermsOfUse className={styles.terms} />
<Button title="action.create" onClick={async () => onSubmitHandler()} />

View file

@ -5,7 +5,6 @@ import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { sendRegisterEmailPasscode } from '@/apis/register';
import { sendSignInEmailPasscode } from '@/apis/sign-in';
import TermsOfUse from '@/containers/TermsOfUse';
import EmailPasswordless from './EmailPasswordless';
@ -31,15 +30,24 @@ describe('<EmailPasswordless/>', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<EmailPasswordless type="sign-in">
<TermsOfUse />
</EmailPasswordless>
<EmailPasswordless type="sign-in" />
</SettingsProvider>
</MemoryRouter>
);
expect(queryByText('description.terms_of_use')).not.toBeNull();
});
test('ender with terms settings but hasTerms param set to false', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<EmailPasswordless type="sign-in" hasTerms={false} />
</SettingsProvider>
</MemoryRouter>
);
expect(queryByText('description.terms_of_use')).toBeNull();
});
test('required email with error message', () => {
const { queryByText, container, getByText } = renderWithPageContext(
<MemoryRouter>
@ -63,14 +71,15 @@ describe('<EmailPasswordless/>', () => {
}
});
test('should block in extra validation failed', async () => {
test('should blocked by terms validation with terms settings enabled', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<EmailPasswordless type="sign-in" onSubmitValidation={async () => false} />
<EmailPasswordless type="sign-in" />
</SettingsProvider>
</MemoryRouter>
);
const emailInput = container.querySelector('input[name="email"]');
if (emailInput) {
@ -88,14 +97,15 @@ describe('<EmailPasswordless/>', () => {
});
});
test('should call sign-in method properly', async () => {
test('should call sign-in method properly with terms settings enabled but hasTerms param set to false', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<EmailPasswordless type="sign-in" />
<EmailPasswordless type="sign-in" hasTerms={false} />
</SettingsProvider>
</MemoryRouter>
);
const emailInput = container.querySelector('input[name="email"]');
if (emailInput) {
@ -113,7 +123,35 @@ describe('<EmailPasswordless/>', () => {
});
});
test('should call register method properly', async () => {
test('should call sign-in method properly with terms settings enabled and checked', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<EmailPasswordless type="sign-in" />
</SettingsProvider>
</MemoryRouter>
);
const emailInput = container.querySelector('input[name="email"]');
if (emailInput) {
fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } });
}
const termsButton = getByText('description.agree_with_terms');
fireEvent.click(termsButton);
const submitButton = getByText('action.continue');
act(() => {
fireEvent.click(submitButton);
});
await waitFor(() => {
expect(sendSignInEmailPasscode).toBeCalledWith('foo@logto.io');
});
});
test('should call register method properly if type is register', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
@ -126,6 +164,10 @@ describe('<EmailPasswordless/>', () => {
if (emailInput) {
fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } });
}
const termsButton = getByText('description.agree_with_terms');
fireEvent.click(termsButton);
const submitButton = getByText('action.continue');
act(() => {

View file

@ -6,14 +6,17 @@ import { useNavigate } from 'react-router-dom';
import { getSendPasscodeApi } from '@/apis/utils';
import Button from '@/components/Button';
import Input from '@/components/Input';
import TermsOfUse from '@/containers/TermsOfUse';
import useApi, { ErrorHandlers } from '@/hooks/use-api';
import useForm from '@/hooks/use-form';
import { PageContext } from '@/hooks/use-page-context';
import useTerms from '@/hooks/use-terms';
import { UserFlow, SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
import { emailValidation } from '@/utils/field-validations';
import PasswordlessConfirmModal from './PasswordlessConfirmModal';
import PasswordlessSwitch from './PasswordlessSwitch';
import * as styles from './index.module.scss';
type Props = {
@ -21,8 +24,8 @@ type Props = {
className?: string;
// eslint-disable-next-line react/boolean-prop-naming
autoFocus?: boolean;
onSubmitValidation?: () => Promise<boolean>;
children?: React.ReactNode;
hasTerms?: boolean;
hasSwitch?: boolean;
};
type FieldState = {
@ -31,10 +34,18 @@ type FieldState = {
const defaultState: FieldState = { email: '' };
const EmailPasswordless = ({ type, autoFocus, onSubmitValidation, children, className }: Props) => {
const EmailPasswordless = ({
type,
autoFocus,
hasTerms = true,
hasSwitch = false,
className,
}: Props) => {
const { setToast } = useContext(PageContext);
const { t } = useTranslation();
const navigate = useNavigate();
const { termsValidation } = useTerms();
const { fieldValue, setFieldValue, setFieldErrors, register, validateForm } =
useForm(defaultState);
@ -75,13 +86,13 @@ const EmailPasswordless = ({ type, autoFocus, onSubmitValidation, children, clas
return;
}
if (onSubmitValidation && !(await onSubmitValidation())) {
if (hasTerms && !(await termsValidation())) {
return;
}
void asyncSendPasscode(fieldValue.email);
},
[validateForm, onSubmitValidation, asyncSendPasscode, fieldValue.email]
[validateForm, hasTerms, termsValidation, asyncSendPasscode, fieldValue.email]
);
const onModalCloseHandler = useCallback(() => {
@ -103,22 +114,24 @@ const EmailPasswordless = ({ type, autoFocus, onSubmitValidation, children, clas
return (
<>
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
<Input
type="email"
name="email"
autoComplete="email"
inputMode="email"
placeholder={t('input.email')}
autoFocus={autoFocus}
className={styles.inputField}
{...register('email', emailValidation)}
onClear={() => {
setFieldValue((state) => ({ ...state, email: '' }));
}}
/>
{children && <div className={styles.childWrapper}>{children}</div>}
<div className={styles.formFields}>
<Input
type="email"
name="email"
autoComplete="email"
inputMode="email"
placeholder={t('input.email')}
autoFocus={autoFocus}
className={styles.inputField}
{...register('email', emailValidation)}
onClear={() => {
setFieldValue((state) => ({ ...state, email: '' }));
}}
/>
{hasSwitch && <PasswordlessSwitch target="sms" className={styles.switch} />}
</div>
{hasTerms && <TermsOfUse className={styles.terms} />}
<Button title="action.continue" onClick={async () => onSubmitHandler()} />
<input hidden type="submit" />

View file

@ -0,0 +1,73 @@
import { fireEvent } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { mockSignInExperienceSettings } from '@/__mocks__/logto';
import PasswordlessSwitch from './PasswordlessSwitch';
const mockedNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockedNavigate,
}));
describe('<PasswordlessSwitch />', () => {
afterEach(() => {
mockedNavigate.mockClear();
});
test('render sms passwordless switch', () => {
const { queryByText, getByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/forgot-password/sms']}>
<SettingsProvider>
<PasswordlessSwitch target="email" />
</SettingsProvider>
</MemoryRouter>
);
expect(queryByText('action.switch_to')).not.toBeNull();
const link = getByText('action.switch_to');
fireEvent.click(link);
expect(mockedNavigate).toBeCalledWith({ pathname: '/forgot-password/email' });
});
test('render email passwordless switch', () => {
const { queryByText, getByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/forgot-password/email']}>
<SettingsProvider>
<PasswordlessSwitch target="sms" />
</SettingsProvider>
</MemoryRouter>
);
expect(queryByText('action.switch_to')).not.toBeNull();
const link = getByText('action.switch_to');
fireEvent.click(link);
expect(mockedNavigate).toBeCalledWith({ pathname: '/forgot-password/sms' });
});
test('should not render the switch if SIE setting does not has the supported sign in method', () => {
const { queryByText, getByText } = renderWithPageContext(
<MemoryRouter initialEntries={['/forgot-password/email']}>
<SettingsProvider
settings={{
...mockSignInExperienceSettings,
primarySignInMethod: 'username',
secondarySignInMethods: ['email', 'social'],
}}
>
<PasswordlessSwitch target="sms" />
</SettingsProvider>
</MemoryRouter>
);
expect(queryByText('action.switch_to')).toBeNull();
});
});

View file

@ -0,0 +1,48 @@
import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useLocation } from 'react-router-dom';
import TextLink from '@/components/TextLink';
import { PageContext } from '@/hooks/use-page-context';
type Props = {
target: 'sms' | 'email';
className?: string;
};
const PasswordlessSwitch = ({ target, className }: Props) => {
const { t } = useTranslation();
const { experienceSettings } = useContext(PageContext);
const { pathname } = useLocation();
const navigate = useNavigate();
if (!experienceSettings) {
return null;
}
if (
experienceSettings.primarySignInMethod !== target &&
!experienceSettings.secondarySignInMethods.includes(target)
) {
return null;
}
const targetPathname = pathname.replace(target === 'email' ? 'sms' : 'email', target);
return (
<TextLink
className={className}
onClick={() => {
navigate({
pathname: targetPathname,
});
}}
>
{t('action.switch_to', {
method: t(`description.${target === 'email' ? 'email' : 'phone_number'}`),
})}
</TextLink>
);
};
export default PasswordlessSwitch;

View file

@ -5,7 +5,6 @@ import renderWithPageContext from '@/__mocks__/RenderWithPageContext';
import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider';
import { sendRegisterSmsPasscode } from '@/apis/register';
import { sendSignInSmsPasscode } from '@/apis/sign-in';
import TermsOfUse from '@/containers/TermsOfUse';
import { getDefaultCountryCallingCode } from '@/utils/country-code';
import PhonePasswordless from './PhonePasswordless';
@ -38,15 +37,24 @@ describe('<PhonePasswordless/>', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<PhonePasswordless type="sign-in">
<TermsOfUse />
</PhonePasswordless>
<PhonePasswordless type="sign-in" />
</SettingsProvider>
</MemoryRouter>
);
expect(queryByText('description.terms_of_use')).not.toBeNull();
});
test('render with terms settings but hasTerms param set to false', () => {
const { queryByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<PhonePasswordless type="sign-in" hasTerms={false} />
</SettingsProvider>
</MemoryRouter>
);
expect(queryByText('description.terms_of_use')).toBeNull();
});
test('required phone with error message', () => {
const { queryByText, container, getByText } = renderWithPageContext(
<MemoryRouter>
@ -70,31 +78,7 @@ describe('<PhonePasswordless/>', () => {
}
});
test('should block if extra validation failed', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<PhonePasswordless type="sign-in" onSubmitValidation={async () => false} />
</SettingsProvider>
</MemoryRouter>
);
const phoneInput = container.querySelector('input[name="phone"]');
if (phoneInput) {
fireEvent.change(phoneInput, { target: { value: phoneNumber } });
}
const submitButton = getByText('action.continue');
act(() => {
fireEvent.click(submitButton);
});
await waitFor(() => {
expect(sendSignInSmsPasscode).not.toBeCalled();
});
});
test('should call sign-in method properly', async () => {
test('should blocked by terms validation with terms settings enabled', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
@ -107,6 +91,32 @@ describe('<PhonePasswordless/>', () => {
if (phoneInput) {
fireEvent.change(phoneInput, { target: { value: phoneNumber } });
}
const submitButton = getByText('action.continue');
act(() => {
fireEvent.click(submitButton);
});
await waitFor(() => {
expect(sendSignInSmsPasscode).not.toBeCalled();
});
});
test('should call sign-in method properly with terms settings enabled but hasTerms param set to false', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<PhonePasswordless type="sign-in" hasTerms={false} />
</SettingsProvider>
</MemoryRouter>
);
const phoneInput = container.querySelector('input[name="phone"]');
if (phoneInput) {
fireEvent.change(phoneInput, { target: { value: phoneNumber } });
}
const submitButton = getByText('action.continue');
act(() => {
@ -118,7 +128,35 @@ describe('<PhonePasswordless/>', () => {
});
});
test('should call register method properly', async () => {
test('should call sign-in method properly with terms settings enabled and checked', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
<PhonePasswordless type="sign-in" />
</SettingsProvider>
</MemoryRouter>
);
const phoneInput = container.querySelector('input[name="phone"]');
if (phoneInput) {
fireEvent.change(phoneInput, { target: { value: phoneNumber } });
}
const termsButton = getByText('description.agree_with_terms');
fireEvent.click(termsButton);
const submitButton = getByText('action.continue');
act(() => {
fireEvent.click(submitButton);
});
await waitFor(() => {
expect(sendSignInSmsPasscode).toBeCalledWith(`${defaultCountryCallingCode}${phoneNumber}`);
});
});
test('should call register method properly if type is register', async () => {
const { container, getByText } = renderWithPageContext(
<MemoryRouter>
<SettingsProvider>
@ -132,6 +170,9 @@ describe('<PhonePasswordless/>', () => {
fireEvent.change(phoneInput, { target: { value: phoneNumber } });
}
const termsButton = getByText('description.agree_with_terms');
fireEvent.click(termsButton);
const submitButton = getByText('action.continue');
act(() => {

View file

@ -6,14 +6,17 @@ import { useNavigate } from 'react-router-dom';
import { getSendPasscodeApi } from '@/apis/utils';
import Button from '@/components/Button';
import { PhoneInput } from '@/components/Input';
import TermsOfUse from '@/containers/TermsOfUse';
import useApi, { ErrorHandlers } from '@/hooks/use-api';
import useForm from '@/hooks/use-form';
import { PageContext } from '@/hooks/use-page-context';
import usePhoneNumber from '@/hooks/use-phone-number';
import useTerms from '@/hooks/use-terms';
import { UserFlow, SearchParameters } from '@/types';
import { getSearchParameters } from '@/utils';
import PasswordlessConfirmModal from './PasswordlessConfirmModal';
import PasswordlessSwitch from './PasswordlessSwitch';
import * as styles from './index.module.scss';
type Props = {
@ -21,8 +24,8 @@ type Props = {
className?: string;
// eslint-disable-next-line react/boolean-prop-naming
autoFocus?: boolean;
onSubmitValidation?: () => Promise<boolean>;
children?: React.ReactNode;
hasTerms?: boolean;
hasSwitch?: boolean;
};
type FieldState = {
@ -31,9 +34,17 @@ type FieldState = {
const defaultState: FieldState = { phone: '' };
const PhonePasswordless = ({ type, autoFocus, onSubmitValidation, children, className }: Props) => {
const PhonePasswordless = ({
type,
autoFocus,
hasTerms = true,
hasSwitch = false,
className,
}: Props) => {
const { setToast } = useContext(PageContext);
const { t } = useTranslation();
const { termsValidation } = useTerms();
const { countryList, phoneNumber, setPhoneNumber, isValidPhoneNumber } = usePhoneNumber();
const navigate = useNavigate();
const { fieldValue, setFieldValue, setFieldErrors, validateForm, register } =
@ -85,13 +96,13 @@ const PhonePasswordless = ({ type, autoFocus, onSubmitValidation, children, clas
return;
}
if (onSubmitValidation && !(await onSubmitValidation())) {
if (hasTerms && !(await termsValidation())) {
return;
}
void asyncSendPasscode(fieldValue.phone);
},
[validateForm, onSubmitValidation, asyncSendPasscode, fieldValue.phone]
[validateForm, hasTerms, termsValidation, asyncSendPasscode, fieldValue.phone]
);
const onModalCloseHandler = useCallback(() => {
@ -118,20 +129,24 @@ const PhonePasswordless = ({ type, autoFocus, onSubmitValidation, children, clas
return (
<>
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
<PhoneInput
name="phone"
placeholder={t('input.phone_number')}
className={styles.inputField}
countryCallingCode={phoneNumber.countryCallingCode}
nationalNumber={phoneNumber.nationalNumber}
autoFocus={autoFocus}
countryList={countryList}
{...register('phone', phoneNumberValidation)}
onChange={(data) => {
setPhoneNumber((previous) => ({ ...previous, ...data }));
}}
/>
{children && <div className={styles.childWrapper}>{children}</div>}
<div className={styles.formFields}>
<PhoneInput
name="phone"
placeholder={t('input.phone_number')}
className={styles.inputField}
countryCallingCode={phoneNumber.countryCallingCode}
nationalNumber={phoneNumber.nationalNumber}
autoFocus={autoFocus}
countryList={countryList}
{...register('phone', phoneNumberValidation)}
onChange={(data) => {
setPhoneNumber((previous) => ({ ...previous, ...data }));
}}
/>
{hasSwitch && <PasswordlessSwitch target="email" className={styles.switch} />}
</div>
{hasTerms && <TermsOfUse className={styles.terms} />}
<Button title="action.continue" onClick={async () => onSubmitHandler()} />

View file

@ -7,11 +7,24 @@
width: 100%;
}
.inputField {
margin-bottom: _.unit(12);
}
.childWrapper {
.inputField,
.terms,
.switch {
margin-bottom: _.unit(4);
}
.switch {
margin-top: _.unit(-1);
display: block;
}
.formFields {
margin-bottom: _.unit(8);
}
}
:global(body.desktop) {
.formFields {
margin-bottom: _.unit(2);
}
}

View file

@ -0,0 +1,5 @@
@use '@/scss/underscore' as _;
.terms {
margin-bottom: _.unit(4);
}

View file

@ -1,7 +1,9 @@
import TermsOfUse from '@/containers/TermsOfUse';
import useNativeMessageListener from '@/hooks/use-native-message-listener';
import useSocial from '@/hooks/use-social';
import SocialSignInList from '../SocialSignInList';
import * as styles from './index.module.scss';
export const defaultSize = 3;
@ -13,7 +15,12 @@ const PrimarySocialSignIn = ({ className }: Props) => {
const { socialConnectors } = useSocial();
useNativeMessageListener();
return <SocialSignInList className={className} socialConnectors={socialConnectors} />;
return (
<>
<TermsOfUse className={styles.terms} />
<SocialSignInList className={className} socialConnectors={socialConnectors} />
</>
);
};
export default PrimarySocialSignIn;

View file

@ -11,16 +11,22 @@
margin-bottom: _.unit(4);
}
.formFields {
margin-bottom: _.unit(8);
}
.terms {
margin: _.unit(8) 0 _.unit(4);
margin-bottom: _.unit(4);
}
.formErrors {
margin-top: _.unit(-2);
margin-bottom: _.unit(4);
+ .terms {
margin-top: _.unit(4);
}
}
}
:global(body.desktop) {
.formFields {
margin-bottom: _.unit(2);
}
}

View file

@ -91,27 +91,30 @@ const UsernameSignIn = ({ className, autoFocus }: Props) => {
return (
<form className={classNames(styles.form, className)} onSubmit={onSubmitHandler}>
<Input
autoFocus={autoFocus}
className={styles.inputField}
name="username"
autoComplete="username"
placeholder={t('input.username')}
{...register('username', (value) => requiredValidation('username', value))}
onClear={() => {
setFieldValue((state) => ({ ...state, username: '' }));
}}
/>
<PasswordInput
className={styles.inputField}
name="password"
autoComplete="current-password"
placeholder={t('input.password')}
{...register('password', (value) => requiredValidation('password', value))}
/>
{formErrorMessage && (
<ErrorMessage className={styles.formErrors}>{formErrorMessage}</ErrorMessage>
)}
<div className={styles.formFields}>
<Input
autoFocus={autoFocus}
className={styles.inputField}
name="username"
autoComplete="username"
placeholder={t('input.username')}
{...register('username', (value) => requiredValidation('username', value))}
onClear={() => {
setFieldValue((state) => ({ ...state, username: '' }));
}}
/>
<PasswordInput
className={styles.inputField}
name="password"
autoComplete="current-password"
placeholder={t('input.password')}
{...register('password', (value) => requiredValidation('password', value))}
/>
{formErrorMessage && (
<ErrorMessage className={styles.formErrors}>{formErrorMessage}</ErrorMessage>
)}
</div>
<TermsOfUse className={styles.terms} />
<Button title="action.sign_in" onClick={async () => onSubmitHandler()} />

View file

@ -14,16 +14,15 @@ type Props = {
const ForgotPassword = () => {
const { t } = useTranslation();
const { method = '' } = useParams<Props>();
const forgotPasswordForm = useMemo(() => {
if (method === 'sms') {
return <PhonePasswordless autoFocus type="reset-password" />;
return <PhonePasswordless autoFocus hasSwitch type="reset-password" hasTerms={false} />;
}
if (method === 'email') {
return <EmailPasswordless autoFocus type="reset-password" />;
return <EmailPasswordless autoFocus hasSwitch type="reset-password" hasTerms={false} />;
}
}, [method]);

View file

@ -5,8 +5,6 @@ import { useParams } from 'react-router-dom';
import NavBar from '@/components/NavBar';
import CreateAccount from '@/containers/CreateAccount';
import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless';
import TermsOfUse from '@/containers/TermsOfUse';
import useTerms from '@/hooks/use-terms';
import ErrorPage from '@/pages/ErrorPage';
import * as styles from './index.module.scss';
@ -19,27 +17,17 @@ const Register = () => {
const { t } = useTranslation();
const { method = 'username' } = useParams<Parameters>();
const { termsValidation } = useTerms();
const registerForm = useMemo(() => {
if (method === 'sms') {
return (
<PhonePasswordless autoFocus type="register" onSubmitValidation={termsValidation}>
<TermsOfUse />
</PhonePasswordless>
);
return <PhonePasswordless autoFocus type="register" />;
}
if (method === 'email') {
return (
<EmailPasswordless autoFocus type="register" onSubmitValidation={termsValidation}>
<TermsOfUse />
</EmailPasswordless>
);
return <EmailPasswordless autoFocus type="register" />;
}
return <CreateAccount autoFocus />;
}, [method, termsValidation]);
}, [method]);
if (!['email', 'sms', 'username'].includes(method)) {
return <ErrorPage />;

View file

@ -4,9 +4,7 @@ import { useParams } from 'react-router-dom';
import NavBar from '@/components/NavBar';
import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless';
import TermsOfUse from '@/containers/TermsOfUse';
import UsernameSignIn from '@/containers/UsernameSignIn';
import useTerms from '@/hooks/use-terms';
import ErrorPage from '@/pages/ErrorPage';
import * as styles from './index.module.scss';
@ -19,27 +17,17 @@ const SecondarySignIn = () => {
const { t } = useTranslation();
const { method = 'username' } = useParams<Props>();
const { termsValidation } = useTerms();
const signInForm = useMemo(() => {
if (method === 'sms') {
return (
<PhonePasswordless autoFocus type="sign-in" onSubmitValidation={termsValidation}>
<TermsOfUse />
</PhonePasswordless>
);
return <PhonePasswordless autoFocus type="sign-in" />;
}
if (method === 'email') {
return (
<EmailPasswordless autoFocus type="sign-in" onSubmitValidation={termsValidation}>
<TermsOfUse />
</EmailPasswordless>
);
return <EmailPasswordless autoFocus type="sign-in" />;
}
return <UsernameSignIn autoFocus />;
}, [method, termsValidation]);
}, [method]);
if (!['email', 'sms', 'username'].includes(method)) {
return <ErrorPage />;

View file

@ -5,10 +5,6 @@
@include _.flex-column(normal, normal);
@include _.full-width;
.terms {
margin-bottom: _.unit(4);
}
.primarySignIn {
margin-bottom: _.unit(5);
}

View file

@ -6,7 +6,6 @@ import CreateAccount from '@/containers/CreateAccount';
import { EmailPasswordless, PhonePasswordless } from '@/containers/Passwordless';
import SignInMethodsLink from '@/containers/SignInMethodsLink';
import { PrimarySocialSignIn, SecondarySocialSignIn } from '@/containers/SocialSignIn';
import TermsOfUse from '@/containers/TermsOfUse';
import UsernameSignIn from '@/containers/UsernameSignIn';
import { SignInMethod, LocalSignInMethod } from '@/types';
@ -44,10 +43,7 @@ export const PrimarySection = ({
);
case 'social':
return socialConnectors.length > 0 ? (
<>
<TermsOfUse className={styles.terms} />
<PrimarySocialSignIn className={styles.primarySocial} />
</>
<PrimarySocialSignIn className={styles.primarySocial} />
) : null;
default:
return null;