0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat(ui): add forget password page (#1943)

* refactor(ui): folder rename temp

folder rename temp

* fix(ui): folder rename

rename UsernameSignin to UsernameSignIn

* feat(ui): add forget-password page

add forget-password page

* test(ui): add page ut

add page ut

* fix(ui): cr update

change forget password to forgot password

* chore(ui): hide WIP page path

hide WIP page path
This commit is contained in:
simeng-li 2022-09-19 16:08:31 +08:00 committed by GitHub
parent de4c46e400
commit 39d80d9912
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 183 additions and 21 deletions

View file

@ -25,6 +25,7 @@ const translation = {
agree: 'Agree', agree: 'Agree',
got_it: 'Got it', got_it: 'Got it',
sign_in_with: 'Sign in with {{name}}', sign_in_with: 'Sign in with {{name}}',
forgot_password: 'Forgot Password?',
}, },
description: { description: {
email: 'email', email: 'email',
@ -35,7 +36,6 @@ const translation = {
agree_with_terms_modal: 'To proceed, please agree to the <link></link>.', agree_with_terms_modal: 'To proceed, please agree to the <link></link>.',
terms_of_use: 'Terms of Use', terms_of_use: 'Terms of Use',
create_account: 'Create Account', create_account: 'Create Account',
forgot_password: 'Forgot Password?',
or: 'or', or: 'or',
enter_passcode: 'The passcode has been sent to your {{address}}', enter_passcode: 'The passcode has been sent to your {{address}}',
passcode_sent: 'The passcode has been resent', passcode_sent: 'The passcode has been resent',
@ -50,6 +50,11 @@ const translation = {
social_create_account: 'No account? You can create a new account and link.', social_create_account: 'No account? You can create a new account and link.',
social_bind_account: 'Already have an account? Sign in to link it with your social identity.', social_bind_account: 'Already have an account? Sign in to link it with your social identity.',
social_bind_with_existing: 'We find a related account, you can link it directly.', social_bind_with_existing: 'We find a related account, you can link it directly.',
reset_password: 'Reset Password',
reset_password_description_email:
'Enter the email address associated with your account, and well email you the verification code to reset your password.',
reset_password_description_sms:
'Enter the phone number associated with your account, and well text you the verification code to reset your password.',
}, },
error: { error: {
username_password_mismatch: 'Username and password do not match', username_password_mismatch: 'Username and password do not match',

View file

@ -27,6 +27,7 @@ const translation = {
agree: 'Accepter', agree: 'Accepter',
got_it: 'Compris', got_it: 'Compris',
sign_in_with: 'Connexion avec {{name}}', sign_in_with: 'Connexion avec {{name}}',
forgot_password: 'Mot de passe oublié ?',
}, },
description: { description: {
email: 'email', email: 'email',
@ -37,7 +38,6 @@ const translation = {
agree_with_terms_modal: 'Pour continuer, veuillez accepter le <link></link>.', agree_with_terms_modal: 'Pour continuer, veuillez accepter le <link></link>.',
terms_of_use: "Conditions d'utilisation", terms_of_use: "Conditions d'utilisation",
create_account: 'Créer un compte', create_account: 'Créer un compte',
forgot_password: 'Mot de passe oublié ?',
or: 'ou', or: 'ou',
enter_passcode: 'Le code a été envoyé à {{address}}', enter_passcode: 'Le code a été envoyé à {{address}}',
passcode_sent: 'Le code a été renvoyé', passcode_sent: 'Le code a été renvoyé',
@ -54,6 +54,11 @@ const translation = {
'Vous avez déjà un compte ? Connectez-vous pour le relier à votre identité sociale.', 'Vous avez déjà un compte ? Connectez-vous pour le relier à votre identité sociale.',
social_bind_with_existing: social_bind_with_existing:
'Nous trouvons un compte connexe, vous pouvez le relier directement.', 'Nous trouvons un compte connexe, vous pouvez le relier directement.',
reset_password: 'Réinitialiser le mot de passe',
reset_password_description_email:
"Entrez l'adresse e-mail associée à votre compte et nous vous enverrons par e-mail le code de vérification pour réinitialiser votre mot de passe.",
reset_password_description_sms:
'Entrez le numéro de téléphone associé à votre compte et nous vous enverrons le code de vérification par SMS pour réinitialiser votre mot de passe.',
}, },
error: { error: {
username_password_mismatch: "Le nom d'utilisateur et le mot de passe ne correspondent pas", username_password_mismatch: "Le nom d'utilisateur et le mot de passe ne correspondent pas",

View file

@ -27,6 +27,7 @@ const translation = {
agree: '동의', agree: '동의',
got_it: '알겠습니다', got_it: '알겠습니다',
sign_in_with: '{{name}} 로그인', sign_in_with: '{{name}} 로그인',
forgot_password: '비밀번호를 잊어버리셨나요?',
}, },
description: { description: {
email: '이메일', email: '이메일',
@ -37,7 +38,6 @@ const translation = {
agree_with_terms_modal: '진행하기 위해서는, 다음을 동의해주세요 <link></link>.', agree_with_terms_modal: '진행하기 위해서는, 다음을 동의해주세요 <link></link>.',
terms_of_use: '이용약관', terms_of_use: '이용약관',
create_account: '계정 생성', create_account: '계정 생성',
forgot_password: '비밀번호를 잊어버리셨나요?',
or: '또는', or: '또는',
enter_passcode: '{{address}} 으로 비밀번호가 전송되었어요.', enter_passcode: '{{address}} 으로 비밀번호가 전송되었어요.',
passcode_sent: '비밀번호가 재전송 되었습니다.', passcode_sent: '비밀번호가 재전송 되었습니다.',
@ -50,6 +50,11 @@ const translation = {
social_create_account: '계정이 없으신가요? 새로운 계정을 만들고 연동해보세요.', social_create_account: '계정이 없으신가요? 새로운 계정을 만들고 연동해보세요.',
social_bind_account: '계정이 이미 있으신가요? 로그인하여 다른 계정과 연동해보세요.', social_bind_account: '계정이 이미 있으신가요? 로그인하여 다른 계정과 연동해보세요.',
social_bind_with_existing: '관련된 계정을 찾았어요. 해당 계정과 연동할 수 있습니다.', social_bind_with_existing: '관련된 계정을 찾았어요. 해당 계정과 연동할 수 있습니다.',
reset_password: '암호를 재설정',
reset_password_description_email:
'계정과 연결된 이메일 주소를 입력하면 비밀번호 재설정을 위한 인증 코드를 이메일로 보내드립니다.',
reset_password_description_sms:
'계정과 연결된 전화번호를 입력하면 비밀번호 재설정을 위한 인증 코드를 문자로 보내드립니다.',
}, },
error: { error: {
username_password_mismatch: '사용자 이름 또는 비밀번호가 일치하지 않아요.', username_password_mismatch: '사용자 이름 또는 비밀번호가 일치하지 않아요.',

View file

@ -27,6 +27,7 @@ const translation = {
agree: 'Aceito', agree: 'Aceito',
got_it: 'Entendi', got_it: 'Entendi',
sign_in_with: 'Entrar com {{name}}', sign_in_with: 'Entrar com {{name}}',
forgot_password: 'Esqueceu a password?',
}, },
description: { description: {
email: 'email', email: 'email',
@ -37,7 +38,6 @@ const translation = {
agree_with_terms_modal: 'Para prosseguir, por favor, concorde com o <link></link>.', agree_with_terms_modal: 'Para prosseguir, por favor, concorde com o <link></link>.',
terms_of_use: 'Termos de uso', terms_of_use: 'Termos de uso',
create_account: 'Criar uma conta', create_account: 'Criar uma conta',
forgot_password: 'Esqueceu a password?',
or: 'ou', or: 'ou',
enter_passcode: 'A senha foi enviada para o seu {{address}}', enter_passcode: 'A senha foi enviada para o seu {{address}}',
passcode_sent: 'A senha foi reenviada', passcode_sent: 'A senha foi reenviada',
@ -50,6 +50,11 @@ const translation = {
social_create_account: 'Sem conta? Pode criar uma nova e agregar.', social_create_account: 'Sem conta? Pode criar uma nova e agregar.',
social_bind_account: 'Já tem uma conta? Faça login para agregar a sua identidade social.', social_bind_account: 'Já tem uma conta? Faça login para agregar a sua identidade social.',
social_bind_with_existing: 'Encontramos uma conta relacionada, pode agrega-la diretamente.', social_bind_with_existing: 'Encontramos uma conta relacionada, pode agrega-la diretamente.',
reset_password: 'Redefinir Password',
reset_password_description_email:
'Digite o endereço de email associado à sua conta e enviaremos um email com o código de verificação para redefinir sua senha.',
reset_password_description_sms:
'Digite o número de telefone associado à sua conta e enviaremos uma mensagem de texto com o código de verificação para redefinir sua senha.',
}, },
error: { error: {
username_password_mismatch: 'O Utilizador e a password não correspondem', username_password_mismatch: 'O Utilizador e a password não correspondem',

View file

@ -27,6 +27,7 @@ const translation = {
agree: 'Kabul Et', agree: 'Kabul Et',
got_it: 'Anladım', got_it: 'Anladım',
sign_in_with: '{{name}} ile giriş yap', sign_in_with: '{{name}} ile giriş yap',
forgot_password: 'Şifremi Unuttum?',
}, },
description: { description: {
email: 'e-posta adresi', email: 'e-posta adresi',
@ -37,7 +38,6 @@ const translation = {
agree_with_terms_modal: 'Devam etmek için lütfen <link></link>i kabul edin.', agree_with_terms_modal: 'Devam etmek için lütfen <link></link>i kabul edin.',
terms_of_use: 'Kullanım Koşulları', terms_of_use: 'Kullanım Koşulları',
create_account: 'Hesap Oluştur', create_account: 'Hesap Oluştur',
forgot_password: 'Şifremi Unuttum?',
or: 'veya', or: 'veya',
enter_passcode: 'Kod {{address}}inize gönderildi.', enter_passcode: 'Kod {{address}}inize gönderildi.',
passcode_sent: 'Kodunuz yeniden gönderildi.', passcode_sent: 'Kodunuz yeniden gönderildi.',
@ -51,6 +51,11 @@ const translation = {
social_create_account: 'Hesabınız yok mu? Yeni bir hesap ve bağlantı oluşturabilirsiniz.', social_create_account: 'Hesabınız yok mu? Yeni bir hesap ve bağlantı oluşturabilirsiniz.',
social_bind_account: 'Hesabınız zaten var mı? Hesabınıza bağlanmak için giriş yapınız.', social_bind_account: 'Hesabınız zaten var mı? Hesabınıza bağlanmak için giriş yapınız.',
social_bind_with_existing: 'İlgili bir hesap bulduk, hemen bağlayabilirsiniz.', social_bind_with_existing: 'İlgili bir hesap bulduk, hemen bağlayabilirsiniz.',
reset_password: 'Şifre yenile',
reset_password_description_email:
'Hesabınızla ilişkili e-posta adresini girin, şifrenizi sıfırlamak için size doğrulama kodunu e-posta ile gönderelim.',
reset_password_description_sms:
'Hesabınızla ilişkili telefon numarasını girin, şifrenizi sıfırlamak için size doğrulama kodunu kısa mesajla gönderelim.',
}, },
error: { error: {
username_password_mismatch: 'Kullanıcı adı ve şifre eşleşmiyor.', username_password_mismatch: 'Kullanıcı adı ve şifre eşleşmiyor.',

View file

@ -27,6 +27,7 @@ const translation = {
agree: '同意', agree: '同意',
got_it: '知道了', got_it: '知道了',
sign_in_with: '通过 {{name}} 登录', sign_in_with: '通过 {{name}} 登录',
forgot_password: '忘记密码?',
}, },
description: { description: {
email: '邮箱', email: '邮箱',
@ -37,7 +38,6 @@ const translation = {
agree_with_terms_modal: '请先同意 <link></link> 以继续', agree_with_terms_modal: '请先同意 <link></link> 以继续',
terms_of_use: '使用条款', terms_of_use: '使用条款',
create_account: '创建帐号', create_account: '创建帐号',
forgot_password: '忘记密码?',
or: '或', or: '或',
enter_passcode: '验证码已经发送至你的{{ address }}', enter_passcode: '验证码已经发送至你的{{ address }}',
passcode_sent: '验证码已经发送', passcode_sent: '验证码已经发送',
@ -50,6 +50,11 @@ const translation = {
social_create_account: '没有帐号?你可以创建一个帐号并绑定。', social_create_account: '没有帐号?你可以创建一个帐号并绑定。',
social_bind_account: '已有帐号?登录以绑定社交身份。', social_bind_account: '已有帐号?登录以绑定社交身份。',
social_bind_with_existing: '找到了一个匹配的帐号,你可以直接绑定。', social_bind_with_existing: '找到了一个匹配的帐号,你可以直接绑定。',
reset_password: '重置密码',
reset_password_description_email:
'输入与你的帐户关联的电子邮箱地址,我们将通过电子邮件向您发送验证码以重置你的密码。',
reset_password_description_sms:
'输入与你的帐户关联的电话号码,我们将向您发送验证码以重置你的密码。',
}, },
error: { error: {
username_password_mismatch: '用户名和密码不匹配', username_password_mismatch: '用户名和密码不匹配',

View file

@ -57,12 +57,21 @@ const App = () => {
/> />
<Route element={<LoadingLayerProvider />}> <Route element={<LoadingLayerProvider />}>
{/* sign-in */}
<Route path="/sign-in" element={<SignIn />} /> <Route path="/sign-in" element={<SignIn />} />
<Route path="/sign-in/social/:connector" element={<SocialSignIn />} /> <Route path="/sign-in/social/:connector" element={<SocialSignIn />} />
<Route path="/sign-in/:method" element={<SecondarySignIn />} /> <Route path="/sign-in/:method" element={<SecondarySignIn />} />
{/* register */}
<Route path="/register" element={<Register />} /> <Route path="/register" element={<Register />} />
<Route path="/register/:method" element={<Register />} /> <Route path="/register/:method" element={<Register />} />
{/* forgot password */}
{/**
* WIP
* <Route path="/forgot-password/:method" element={<ForgotPassword />} />
*/}
{/* social sign-in pages */} {/* social sign-in pages */}
<Route path="/callback/:connector" element={<Callback />} /> <Route path="/callback/:connector" element={<Callback />} />

View file

@ -8,7 +8,7 @@ import { termsOfUseConfirmModalPromise } from '@/containers/TermsOfUse/TermsOfUs
import { termsOfUseIframeModalPromise } from '@/containers/TermsOfUse/TermsOfUseIframeModal'; import { termsOfUseIframeModalPromise } from '@/containers/TermsOfUse/TermsOfUseIframeModal';
import { TermsOfUseModalMessage } from '@/types'; import { TermsOfUseModalMessage } from '@/types';
import UsernameSignin from '.'; import UsernameSignIn from '.';
jest.mock('@/apis/sign-in', () => ({ signInBasic: jest.fn(async () => 0) })); jest.mock('@/apis/sign-in', () => ({ signInBasic: jest.fn(async () => 0) }));
jest.mock('@/containers/TermsOfUse/TermsOfUseConfirmModal', () => ({ jest.mock('@/containers/TermsOfUse/TermsOfUseConfirmModal', () => ({
@ -21,14 +21,14 @@ jest.mock('@/containers/TermsOfUse/TermsOfUseIframeModal', () => ({
const termsOfUseConfirmModalPromiseMock = termsOfUseConfirmModalPromise as jest.Mock; const termsOfUseConfirmModalPromiseMock = termsOfUseConfirmModalPromise as jest.Mock;
const termsOfUseIframeModalPromiseMock = termsOfUseIframeModalPromise as jest.Mock; const termsOfUseIframeModalPromiseMock = termsOfUseIframeModalPromise as jest.Mock;
describe('<UsernameSignin>', () => { describe('<UsernameSignIn>', () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
jest.resetAllMocks(); jest.resetAllMocks();
}); });
test('render', () => { test('render', () => {
const { queryByText, container } = renderWithPageContext(<UsernameSignin />); const { queryByText, container } = renderWithPageContext(<UsernameSignIn />);
expect(container.querySelector('input[name="username"]')).not.toBeNull(); expect(container.querySelector('input[name="username"]')).not.toBeNull();
expect(container.querySelector('input[name="password"]')).not.toBeNull(); expect(container.querySelector('input[name="password"]')).not.toBeNull();
expect(queryByText('action.sign_in')).not.toBeNull(); expect(queryByText('action.sign_in')).not.toBeNull();
@ -37,14 +37,14 @@ describe('<UsernameSignin>', () => {
test('render with terms settings enabled', () => { test('render with terms settings enabled', () => {
const { queryByText } = renderWithPageContext( const { queryByText } = renderWithPageContext(
<SettingsProvider> <SettingsProvider>
<UsernameSignin /> <UsernameSignIn />
</SettingsProvider> </SettingsProvider>
); );
expect(queryByText('description.agree_with_terms')).not.toBeNull(); expect(queryByText('description.agree_with_terms')).not.toBeNull();
}); });
test('required inputs with error message', () => { test('required inputs with error message', () => {
const { queryByText, getByText, container } = renderWithPageContext(<UsernameSignin />); const { queryByText, getByText, container } = renderWithPageContext(<UsernameSignIn />);
const submitButton = getByText('action.sign_in'); const submitButton = getByText('action.sign_in');
fireEvent.click(submitButton); fireEvent.click(submitButton);
@ -72,7 +72,7 @@ describe('<UsernameSignin>', () => {
test('should show terms confirm modal', async () => { test('should show terms confirm modal', async () => {
const { getByText, container } = renderWithPageContext( const { getByText, container } = renderWithPageContext(
<SettingsProvider> <SettingsProvider>
<UsernameSignin /> <UsernameSignIn />
</SettingsProvider> </SettingsProvider>
); );
const submitButton = getByText('action.sign_in'); const submitButton = getByText('action.sign_in');
@ -102,7 +102,7 @@ describe('<UsernameSignin>', () => {
const { getByText, container } = renderWithPageContext( const { getByText, container } = renderWithPageContext(
<SettingsProvider> <SettingsProvider>
<UsernameSignin /> <UsernameSignIn />
</SettingsProvider> </SettingsProvider>
); );
const submitButton = getByText('action.sign_in'); const submitButton = getByText('action.sign_in');
@ -132,7 +132,7 @@ describe('<UsernameSignin>', () => {
test('submit form', async () => { test('submit form', async () => {
const { getByText, container } = renderWithPageContext( const { getByText, container } = renderWithPageContext(
<SettingsProvider> <SettingsProvider>
<UsernameSignin /> <UsernameSignIn />
</SettingsProvider> </SettingsProvider>
); );
const submitButton = getByText('action.sign_in'); const submitButton = getByText('action.sign_in');

View file

@ -32,7 +32,7 @@ const defaultState: FieldState = {
password: '', password: '',
}; };
const UsernameSignin = ({ className, autoFocus }: Props) => { const UsernameSignIn = ({ className, autoFocus }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { termsValidation } = useTerms(); const { termsValidation } = useTerms();
const { const {
@ -121,4 +121,4 @@ const UsernameSignin = ({ className, autoFocus }: Props) => {
); );
}; };
export default UsernameSignin; export default UsernameSignIn;

View file

@ -0,0 +1,44 @@
@use '@/scss/underscore' as _;
.wrapper {
@include _.full-page;
}
.container {
@include _.full-width;
margin-top: _.unit(2);
}
.title {
@include _.title;
}
.description {
margin-bottom: _.unit(6);
color: var(--color-caption);
}
:global(body.mobile) {
.container {
margin-top: _.unit(2);
}
.title {
margin-bottom: _.unit(6);
}
}
:global(body.desktop) {
.container {
margin-top: _.unit(12);
}
.title {
font: var(--font-title-medium);
margin-bottom: _.unit(4);
}
.description {
font: var(--font-caption);
}
}

View file

@ -0,0 +1,32 @@
import { render } from '@testing-library/react';
import { Routes, Route, MemoryRouter } from 'react-router-dom';
import ForgotPassword from '.';
describe('ForgotPassword', () => {
it('render email forgot password properly', () => {
const { queryByText } = render(
<MemoryRouter initialEntries={['/forgot-password/email']}>
<Routes>
<Route path="/forgot-password/:method" element={<ForgotPassword />} />
</Routes>
</MemoryRouter>
);
expect(queryByText('description.reset_password')).not.toBeNull();
expect(queryByText('description.reset_password_description_email')).not.toBeNull();
});
it('render sms forgot password properly', () => {
const { queryByText } = render(
<MemoryRouter initialEntries={['/forgot-password/sms']}>
<Routes>
<Route path="/forgot-password/:method" element={<ForgotPassword />} />
</Routes>
</MemoryRouter>
);
expect(queryByText('description.reset_password')).not.toBeNull();
expect(queryByText('description.reset_password_description_sms')).not.toBeNull();
});
});

View file

@ -0,0 +1,47 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import NavBar from '@/components/NavBar';
import ErrorPage from '@/pages/ErrorPage';
import * as styles from './index.module.scss';
type Props = {
method?: string;
};
const ForgotPassword = () => {
const { t } = useTranslation();
const { method = '' } = useParams<Props>();
const forgotPasswordForm = useMemo(() => {
if (method === 'sms') {
return <div>Phone Number form</div>;
}
if (method === 'email') {
return <div>Email Form</div>;
}
}, [method]);
if (!['email', 'sms'].includes(method)) {
return <ErrorPage />;
}
return (
<div className={styles.wrapper}>
<NavBar />
<div className={styles.container}>
<div className={styles.title}>{t('description.reset_password')}</div>
<div className={styles.description}>
{t(`description.reset_password_description_${method === 'email' ? 'email' : 'sms'}`)}
</div>
{forgotPasswordForm}
</div>
</div>
);
};
export default ForgotPassword;

View file

@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom';
import NavBar from '@/components/NavBar'; import NavBar from '@/components/NavBar';
import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless'; import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless';
import UsernameSignin from '@/containers/UsernameSignin'; import UsernameSignIn from '@/containers/UsernameSignIn';
import ErrorPage from '@/pages/ErrorPage'; import ErrorPage from '@/pages/ErrorPage';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
@ -26,7 +26,7 @@ const SecondarySignIn = () => {
return <EmailPasswordless autoFocus type="sign-in" />; return <EmailPasswordless autoFocus type="sign-in" />;
} }
return <UsernameSignin autoFocus />; return <UsernameSignIn autoFocus />;
}, [method]); }, [method]);
if (!['email', 'sms', 'username'].includes(method)) { if (!['email', 'sms', 'username'].includes(method)) {

View file

@ -7,7 +7,7 @@ import { EmailPasswordless, PhonePasswordless } from '@/containers/Passwordless'
import SignInMethodsLink from '@/containers/SignInMethodsLink'; import SignInMethodsLink from '@/containers/SignInMethodsLink';
import { PrimarySocialSignIn, SecondarySocialSignIn } from '@/containers/SocialSignIn'; import { PrimarySocialSignIn, SecondarySocialSignIn } from '@/containers/SocialSignIn';
import TermsOfUse from '@/containers/TermsOfUse'; import TermsOfUse from '@/containers/TermsOfUse';
import UsernameSignin from '@/containers/UsernameSignin'; import UsernameSignIn from '@/containers/UsernameSignIn';
import { SignInMethod, LocalSignInMethod } from '@/types'; import { SignInMethod, LocalSignInMethod } from '@/types';
import * as styles from './index.module.scss'; import * as styles from './index.module.scss';
@ -40,7 +40,7 @@ export const PrimarySection = ({
return signInMode === SignInMode.Register ? ( return signInMode === SignInMode.Register ? (
<CreateAccount /> <CreateAccount />
) : ( ) : (
<UsernameSignin className={styles.primarySignIn} /> <UsernameSignIn className={styles.primarySignIn} />
); );
case 'social': case 'social':
return socialConnectors.length > 0 ? ( return socialConnectors.length > 0 ? (

View file

@ -3,7 +3,7 @@ import { SignInExperience, ConnectorMetadata, AppearanceMode } from '@logto/sche
export type UserFlow = 'sign-in' | 'register'; export type UserFlow = 'sign-in' | 'register';
export type SignInMethod = 'username' | 'email' | 'sms' | 'social'; export type SignInMethod = 'username' | 'email' | 'sms' | 'social';
export type LocalSignInMethod = 'username' | 'email' | 'sms'; export type LocalSignInMethod = Exclude<SignInMethod, 'social'>;
export enum SearchParameters { export enum SearchParameters {
bindWithSocial = 'bind_with', bindWithSocial = 'bind_with',