From c6aa5dd4e55c8cb07258a2a8660a979fc28597db Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 31 Oct 2022 18:18:06 +0800 Subject: [PATCH] refactor(ui): add primary register page (#2282) --- packages/phrases-ui/src/locales/en.ts | 5 +- packages/phrases-ui/src/locales/fr.ts | 5 +- packages/phrases-ui/src/locales/ko.ts | 5 +- packages/phrases-ui/src/locales/pt-pt.ts | 5 +- packages/phrases-ui/src/locales/tr-tr.ts | 5 +- packages/phrases-ui/src/locales/zh-cn.ts | 3 + .../containers/SignInMethodsLink/index.tsx | 29 +++---- .../containers/SocialCreateAccount/index.tsx | 2 +- .../src/containers/UsernameRegister/index.tsx | 9 ++ packages/ui/src/pages/Register/Main.tsx | 35 ++++++++ .../ui/src/pages/Register/index.module.scss | 43 ++++++++++ packages/ui/src/pages/Register/index.test.tsx | 82 +++++++++++++++++++ packages/ui/src/pages/Register/index.tsx | 52 ++++++++++-- packages/ui/src/pages/SignIn/Main.tsx | 6 +- packages/ui/src/pages/SignIn/index.tsx | 17 ++-- 15 files changed, 259 insertions(+), 44 deletions(-) create mode 100644 packages/ui/src/containers/UsernameRegister/index.tsx create mode 100644 packages/ui/src/pages/Register/Main.tsx create mode 100644 packages/ui/src/pages/Register/index.module.scss create mode 100644 packages/ui/src/pages/Register/index.test.tsx diff --git a/packages/phrases-ui/src/locales/en.ts b/packages/phrases-ui/src/locales/en.ts index a69926ca7..6f680478e 100644 --- a/packages/phrases-ui/src/locales/en.ts +++ b/packages/phrases-ui/src/locales/en.ts @@ -8,6 +8,7 @@ const translation = { }, secondary: { sign_in_with: 'Sign in with {{methods, list(type: disjunction;)}}', + register_with: 'Create account with {{methods, list(type: disjunction;)}}', social_bind_with: 'Already have an account? Sign in to link {{methods, list(type: disjunction;)}} with your social identity.', }, @@ -25,7 +26,7 @@ const translation = { nav_back: 'Back', agree: 'Agree', got_it: 'Got it', - sign_in_with: 'Sign in with {{name}}', + sign_in_with: 'Continue with {{name}}', forgot_password: 'Forgot your password?', switch_to: 'Switch to {{method}}', }, @@ -60,6 +61,8 @@ const translation = { 'Enter the phone number associated with your account, and we’ll message you the verification code to reset your password.', new_password: 'New password', password_changed: 'Password Changed', + no_account: "Don't have an account?", + have_account: 'Already have an account?', }, error: { username_password_mismatch: 'Username and password do not match', diff --git a/packages/phrases-ui/src/locales/fr.ts b/packages/phrases-ui/src/locales/fr.ts index ea6501686..91de102d5 100644 --- a/packages/phrases-ui/src/locales/fr.ts +++ b/packages/phrases-ui/src/locales/fr.ts @@ -10,6 +10,7 @@ const translation = { }, secondary: { sign_in_with: 'Se connecter avec {{methods, list(type: disjunction;)}}', + register_with: 'Create account with {{methods, list(type: disjunction;)}}', // UNTRANSLATED social_bind_with: 'Vous avez déjà un compte ? Connectez-vous pour lier {{methods, list(type: disjunction;)}} avec votre identité sociale.', }, @@ -27,7 +28,7 @@ const translation = { nav_back: 'Retour', agree: 'Accepter', got_it: 'Compris', - sign_in_with: 'Connexion avec {{name}}', + sign_in_with: 'Continuer avec {{name}}', forgot_password: 'Mot de passe oublié ?', switch_to: 'Passer au {{method}}', }, @@ -64,6 +65,8 @@ const translation = { '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.', new_password: 'Nouveau mot de passe', password_changed: 'Password Changed', // UNTRANSLATED + no_account: "Don't have an account?", // UNTRANSLATED + have_account: 'Already have an account?', // UNTRANSLATED }, error: { username_password_mismatch: "Le nom d'utilisateur et le mot de passe ne correspondent pas", diff --git a/packages/phrases-ui/src/locales/ko.ts b/packages/phrases-ui/src/locales/ko.ts index d2c54ba00..11eed97cd 100644 --- a/packages/phrases-ui/src/locales/ko.ts +++ b/packages/phrases-ui/src/locales/ko.ts @@ -10,6 +10,7 @@ const translation = { }, secondary: { sign_in_with: '{{methods, list(type: disjunction;)}} 로그인', + register_with: 'Create account with {{methods, list(type: disjunction;)}}', // UNTRANSLATED social_bind_with: '이미 계정이 있으신가요? {{methods, list(type: disjunction;)}}로 로그인 해보세요!', }, @@ -27,7 +28,7 @@ const translation = { nav_back: '뒤로', agree: '동의', got_it: '알겠습니다', - sign_in_with: '{{name}} 로그인', + sign_in_with: '{{name}} 계속', forgot_password: '비밀번호를 잊어버리셨나요?', switch_to: 'Switch to {{method}}', // UNTRANSLATED }, @@ -60,6 +61,8 @@ const translation = { '계정과 연결된 전화번호를 입력하면 비밀번호 재설정을 위한 인증 코드를 문자로 보내드립니다.', new_password: '새 비밀번호', password_changed: 'Password Changed', // UNTRANSLATED + no_account: "Don't have an account?", // UNTRANSLATED + have_account: 'Already have an account?', // UNTRANSLATED }, error: { username_password_mismatch: '사용자 이름 또는 비밀번호가 일치하지 않아요.', diff --git a/packages/phrases-ui/src/locales/pt-pt.ts b/packages/phrases-ui/src/locales/pt-pt.ts index da4737952..ea73e2442 100644 --- a/packages/phrases-ui/src/locales/pt-pt.ts +++ b/packages/phrases-ui/src/locales/pt-pt.ts @@ -10,6 +10,7 @@ const translation = { }, secondary: { sign_in_with: 'Entrar com {{methods, list(type: disjunction;)}}', + register_with: 'Create account with {{methods, list(type: disjunction;)}}', // UNTRANSLATED social_bind_with: 'Já tem uma conta? Faça login para agregar {{methods, list(type: disjunction;)}} com a sua identidade social.', }, @@ -27,7 +28,7 @@ const translation = { nav_back: 'Anterior', agree: 'Aceito', got_it: 'Entendi', - sign_in_with: 'Entrar com {{name}}', + sign_in_with: 'Continuar com {{name}}', forgot_password: 'Esqueceu a password?', switch_to: 'Mudar para {{method}}', }, @@ -60,6 +61,8 @@ const translation = { '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.', new_password: 'Nova Senha', password_changed: 'Password Changed', // UNTRANSLATED + no_account: "Don't have an account?", // UNTRANSLATED + have_account: 'Already have an account?', // UNTRANSLATED }, error: { username_password_mismatch: 'O Utilizador e a password não correspondem', diff --git a/packages/phrases-ui/src/locales/tr-tr.ts b/packages/phrases-ui/src/locales/tr-tr.ts index 3ae6a26f7..ead6befb4 100644 --- a/packages/phrases-ui/src/locales/tr-tr.ts +++ b/packages/phrases-ui/src/locales/tr-tr.ts @@ -10,6 +10,7 @@ const translation = { }, secondary: { sign_in_with: '{{methods, list(type: disjunction;)}} ile giriş yapınız', + register_with: 'Create account with {{methods, list(type: disjunction;)}}', // UNTRANSLATED social_bind_with: 'Hesabınız zaten var mı? {{methods, list(type: disjunction;)}} bağlantısına tıklayarak giriş yapabilirsiniz', }, @@ -27,7 +28,7 @@ const translation = { nav_back: 'Geri', agree: 'Kabul Et', got_it: 'Anladım', - sign_in_with: '{{name}} ile giriş yap', + sign_in_with: '{{name}} ile ilerle', forgot_password: 'Şifremi Unuttum?', switch_to: 'Switch to {{method}}', // UNTRANSLATED }, @@ -61,6 +62,8 @@ const translation = { 'Hesabınızla ilişkili telefon numarasını girin, şifrenizi sıfırlamak için size doğrulama kodunu kısa mesajla gönderelim.', new_password: 'Yeni Şifre', password_changed: 'Password Changed', // UNTRANSLATED + no_account: "Don't have an account?", // UNTRANSLATED + have_account: 'Already have an account?', // UNTRANSLATED }, error: { username_password_mismatch: 'Kullanıcı adı ve şifre eşleşmiyor.', diff --git a/packages/phrases-ui/src/locales/zh-cn.ts b/packages/phrases-ui/src/locales/zh-cn.ts index 707459fae..3cf0073af 100644 --- a/packages/phrases-ui/src/locales/zh-cn.ts +++ b/packages/phrases-ui/src/locales/zh-cn.ts @@ -10,6 +10,7 @@ const translation = { }, secondary: { sign_in_with: '通过 {{methods, list(type: disjunction;), zhOrSpaces}} 登录', + register_with: 'Create account with {{methods, list(type: disjunction;)}}', // UNTRANSLATED social_bind_with: '绑定到已有账户? 使用 {{methods, list(type: disjunction;), zhOrSpaces}} 登录并绑定。', }, @@ -58,6 +59,8 @@ const translation = { reset_password_description_sms: '输入手机号,领取验证码以重设密码。', new_password: '新密码', password_changed: '已重置密码!', + no_account: "Don't have an account?", // UNTRANSLATED + have_account: 'Already have an account?', // UNTRANSLATED }, error: { username_password_mismatch: '用户名和密码不匹配', diff --git a/packages/ui/src/containers/SignInMethodsLink/index.tsx b/packages/ui/src/containers/SignInMethodsLink/index.tsx index 8e04d4487..63eac0501 100644 --- a/packages/ui/src/containers/SignInMethodsLink/index.tsx +++ b/packages/ui/src/containers/SignInMethodsLink/index.tsx @@ -1,4 +1,3 @@ -import type { SignIn } from '@logto/schemas'; import { SignInIdentifier } from '@logto/schemas'; import classNames from 'classnames'; import type { ReactNode } from 'react'; @@ -12,11 +11,11 @@ import TextLink from '@/components/TextLink'; import * as styles from './index.module.scss'; type Props = { - signInMethods: SignIn['methods']; + methods: SignInIdentifier[]; // Allows social page to pass additional query params to the sign-in pages search?: string; className?: string; - template?: TFuncKey<'translation', 'secondary'>; + template: TFuncKey<'translation', 'secondary'>; }; const SignInMethodsKeyMap: { @@ -27,13 +26,12 @@ const SignInMethodsKeyMap: { [SignInIdentifier.Sms]: 'phone_number', }; -const SignInMethodsLink = ({ signInMethods, template, search, className }: Props) => { +const SignInMethodsLink = ({ methods, template, search, className }: Props) => { const { t } = useTranslation(); - const identifiers = signInMethods.map(({ identifier }) => identifier); - const signInMethodsLink = useMemo( + const methodsLink = useMemo( () => - identifiers.map((identifier) => ( + methods.map((identifier) => ( )), - [identifiers, search] + [methods, search] ); - if (signInMethodsLink.length === 0) { + if (methodsLink.length === 0) { return null; } - // Without text template - if (!template) { - return
{signInMethodsLink}
; - } + // Raw i18n text + const rawText = t(`secondary.${template}`, { methods }); - // With text template - const rawText = t(`secondary.${template}`, { methods: identifiers }); - const textWithLink: ReactNode = identifiers.reduce( + // Replace with link element + const textWithLink: ReactNode = methods.reduce( (content, identifier, index) => // @ts-expect-error: reactStringReplace type bug, using deprecated ReactNodeArray as its input type - reactStringReplace(content, identifier, () => signInMethodsLink[index]), + reactStringReplace(content, identifier, () => methodsLink[index]), rawText ); diff --git a/packages/ui/src/containers/SocialCreateAccount/index.tsx b/packages/ui/src/containers/SocialCreateAccount/index.tsx index 455a290d0..a969299fa 100644 --- a/packages/ui/src/containers/SocialCreateAccount/index.tsx +++ b/packages/ui/src/containers/SocialCreateAccount/index.tsx @@ -43,7 +43,7 @@ const SocialCreateAccount = ({ connectorId, className }: Props) => { }} /> identifier)} template="social_bind_with" className={styles.desc} search={queryStringify({ [SearchParameters.bindWithSocial]: connectorId })} diff --git a/packages/ui/src/containers/UsernameRegister/index.tsx b/packages/ui/src/containers/UsernameRegister/index.tsx new file mode 100644 index 000000000..983a5fb5f --- /dev/null +++ b/packages/ui/src/containers/UsernameRegister/index.tsx @@ -0,0 +1,9 @@ +type Props = { + className?: string; +}; + +const UsernameRegister = ({ className }: Props) => ( +
username register
+); + +export default UsernameRegister; diff --git a/packages/ui/src/pages/Register/Main.tsx b/packages/ui/src/pages/Register/Main.tsx new file mode 100644 index 000000000..a715ccf71 --- /dev/null +++ b/packages/ui/src/pages/Register/Main.tsx @@ -0,0 +1,35 @@ +import type { SignInIdentifier, ConnectorMetadata } from '@logto/schemas'; + +import { EmailPasswordless, PhonePasswordless } from '@/containers/Passwordless'; +import SocialSignIn from '@/containers/SocialSignIn'; +import UsernameRegister from '@/containers/UsernameRegister'; + +import * as styles from './index.module.scss'; + +type Props = { + signUpMethod?: SignInIdentifier; + socialConnectors: ConnectorMetadata[]; +}; + +const Main = ({ signUpMethod, socialConnectors }: Props) => { + switch (signUpMethod) { + case 'email': + return ; + + case 'sms': + return ; + + case 'username': + return ; + + default: { + if (socialConnectors.length > 0) { + return ; + } + + return null; + } + } +}; + +export default Main; diff --git a/packages/ui/src/pages/Register/index.module.scss b/packages/ui/src/pages/Register/index.module.scss new file mode 100644 index 000000000..33e8bdab4 --- /dev/null +++ b/packages/ui/src/pages/Register/index.module.scss @@ -0,0 +1,43 @@ +@use '@/scss/underscore' as _; + +.main { + margin-bottom: _.unit(4); +} + +.otherMethodsLink { + margin-bottom: _.unit(6); +} + +.createAccount { + margin-top: _.unit(6); + text-align: center; +} + +.placeHolder { + flex: 1; +} + + +:global(body.mobile) { + .divider { + margin-bottom: _.unit(5); + } + + .createAccount { + padding-bottom: env(safe-area-inset-bottom); + } +} + +:global(body.desktop) { + .main { + margin-bottom: _.unit(6); + } + + .placeHolder { + flex: 0; + } + + .divider { + margin-bottom: _.unit(4); + } +} diff --git a/packages/ui/src/pages/Register/index.test.tsx b/packages/ui/src/pages/Register/index.test.tsx new file mode 100644 index 000000000..d1d5bf41f --- /dev/null +++ b/packages/ui/src/pages/Register/index.test.tsx @@ -0,0 +1,82 @@ +import { SignInIdentifier } from '@logto/schemas'; +import { MemoryRouter } from 'react-router-dom'; + +import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; +import SettingsProvider from '@/__mocks__/RenderWithPageContext/SettingsProvider'; +import { mockSignInExperienceSettings } from '@/__mocks__/logto'; +import { defaultSize } from '@/containers/SocialSignIn/SocialSignInList'; +import Register from '@/pages/Register'; + +jest.mock('i18next', () => ({ + language: 'en', +})); + +describe('', () => { + test('renders with username as primary', async () => { + const { queryByText, queryAllByText } = renderWithPageContext( + + + + + + ); + + expect(queryByText('username register')).not.toBeNull(); + + // Social + expect(queryAllByText('action.sign_in_with')).toHaveLength(defaultSize); + }); + + test('renders with email passwordless as primary', async () => { + const { queryByText, container } = renderWithPageContext( + + + + + + ); + expect(container.querySelector('input[name="email"]')).not.toBeNull(); + expect(queryByText('action.continue')).not.toBeNull(); + }); + + test('renders with sms passwordless as primary', async () => { + const { queryByText, container } = renderWithPageContext( + + + + + + ); + expect(container.querySelector('input[name="phone"]')).not.toBeNull(); + expect(queryByText('action.continue')).not.toBeNull(); + }); + + test('renders with social as primary', async () => { + const { container } = renderWithPageContext( + + + + + + ); + + expect(container.querySelectorAll('button')).toHaveLength( + mockSignInExperienceSettings.socialConnectors.length + ); + }); +}); diff --git a/packages/ui/src/pages/Register/index.tsx b/packages/ui/src/pages/Register/index.tsx index 349115c10..f6789b9af 100644 --- a/packages/ui/src/pages/Register/index.tsx +++ b/packages/ui/src/pages/Register/index.tsx @@ -1,16 +1,52 @@ -import { useContext } from 'react'; +import { useTranslation } from 'react-i18next'; +import Divider from '@/components/Divider'; +import TextLink from '@/components/TextLink'; import LandingPageContainer from '@/containers/LandingPageContainer'; -import { PageContext } from '@/hooks/use-page-context'; +import SignInMethodsLink from '@/containers/SignInMethodsLink'; +import { SocialSignInList } from '@/containers/SocialSignIn'; +import { useSieMethods } from '@/hooks/use-sie'; + +import Main from './Main'; +import * as styles from './index.module.scss'; const Register = () => { - const { experienceSettings } = useContext(PageContext); + const { signUpMethods, socialConnectors } = useSieMethods(); + const otherMethods = signUpMethods.slice(1); + const { t } = useTranslation(); - if (!experienceSettings) { - return null; - } - - return signUp; + return ( + +
+ { + // Other create account methods + otherMethods.length > 0 && ( + + ) + } + { + // Social sign-in methods + signUpMethods.length > 0 && socialConnectors.length > 0 && ( + <> + + + + ) + } + { + // SignIn footer + signUpMethods.length > 0 && ( + <> +
+
+ {t('description.have_account')}{' '} + +
+ + ) + } + + ); }; export default Register; diff --git a/packages/ui/src/pages/SignIn/Main.tsx b/packages/ui/src/pages/SignIn/Main.tsx index 794dcbcc5..46b1c0ca1 100644 --- a/packages/ui/src/pages/SignIn/Main.tsx +++ b/packages/ui/src/pages/SignIn/Main.tsx @@ -15,11 +15,7 @@ type Props = { }; const Main = ({ signInMethod, socialConnectors }: Props) => { - if (!signInMethod) { - return socialConnectors.length > 0 ? : null; - } - - switch (signInMethod.identifier) { + switch (signInMethod?.identifier) { case 'email': { if (signInMethod.password && !signInMethod.verificationCode) { return ; diff --git a/packages/ui/src/pages/SignIn/index.tsx b/packages/ui/src/pages/SignIn/index.tsx index abb35e3b3..85aec0ac8 100644 --- a/packages/ui/src/pages/SignIn/index.tsx +++ b/packages/ui/src/pages/SignIn/index.tsx @@ -1,3 +1,5 @@ +import { useTranslation } from 'react-i18next'; + import Divider from '@/components/Divider'; import TextLink from '@/components/TextLink'; import LandingPageContainer from '@/containers/LandingPageContainer'; @@ -10,7 +12,8 @@ import * as styles from './index.module.scss'; const SignIn = () => { const { signInMethods, signUpMethods, socialConnectors } = useSieMethods(); - const otherMethods = signInMethods.slice(1); + const otherMethods = signInMethods.slice(1).map(({ identifier }) => identifier); + const { t } = useTranslation(); return ( @@ -18,7 +21,7 @@ const SignIn = () => { { // Other sign-in methods otherMethods.length > 0 && ( - + ) } { @@ -35,12 +38,10 @@ const SignIn = () => { signUpMethods.length > 0 && ( <>
- +
+ {t('description.no_account')}{' '} + +
) }