From e1d3d345234eee3bbb93f9970821ce7bd9513f00 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 31 Oct 2022 10:39:41 +0800 Subject: [PATCH] refactor(ui): refactor sign-in page (#2275) --- packages/ui/package.json | 4 +- packages/ui/src/App.tsx | 3 +- packages/ui/src/components/TextLink/index.tsx | 6 +- .../ui/src/containers/EmailPassword/index.tsx | 9 +++ .../LandingPageContainer/index.module.scss | 28 ++++++++ .../containers/LandingPageContainer/index.tsx | 44 ++++++++++++ .../Passwordless/PasswordlessSwitch.test.tsx | 31 ++------ .../Passwordless/PasswordlessSwitch.tsx | 12 +--- .../ui/src/containers/PhonePassword/index.tsx | 9 +++ .../containers/SignInMethodsLink/index.tsx | 40 +++++------ .../containers/SocialCreateAccount/index.tsx | 7 +- .../SocialSignInList/index.test.tsx | 6 +- .../SocialSignIn/SocialSignInList/index.tsx | 9 +-- .../ui/src/containers/SocialSignIn/index.tsx | 4 +- packages/ui/src/hooks/use-bind-social.ts | 12 +--- packages/ui/src/hooks/use-sie.ts | 13 ++++ packages/ui/src/pages/Register/index.tsx | 48 ++----------- .../index.module.scss | 0 .../index.test.tsx | 12 ++-- .../ui/src/pages/SecondaryRegister/index.tsx | 50 +++++++++++++ packages/ui/src/pages/SignIn/Main.tsx | 53 ++++++++++++++ packages/ui/src/pages/SignIn/MainForm.tsx | 38 ---------- .../ui/src/pages/SignIn/index.module.scss | 52 ++++---------- packages/ui/src/pages/SignIn/index.test.tsx | 65 +++++++++++++++-- packages/ui/src/pages/SignIn/index.tsx | 70 +++++++++++-------- packages/ui/src/types/index.ts | 6 ++ pnpm-lock.yaml | 5 +- 27 files changed, 383 insertions(+), 253 deletions(-) create mode 100644 packages/ui/src/containers/EmailPassword/index.tsx create mode 100644 packages/ui/src/containers/LandingPageContainer/index.module.scss create mode 100644 packages/ui/src/containers/LandingPageContainer/index.tsx create mode 100644 packages/ui/src/containers/PhonePassword/index.tsx create mode 100644 packages/ui/src/hooks/use-sie.ts rename packages/ui/src/pages/{Register => SecondaryRegister}/index.module.scss (100%) rename packages/ui/src/pages/{Register => SecondaryRegister}/index.test.tsx (81%) create mode 100644 packages/ui/src/pages/SecondaryRegister/index.tsx create mode 100644 packages/ui/src/pages/SignIn/Main.tsx delete mode 100644 packages/ui/src/pages/SignIn/MainForm.tsx diff --git a/packages/ui/package.json b/packages/ui/package.json index 685d3cd6c..051620f83 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -15,10 +15,8 @@ "test:ci": "jest --coverage --silent", "test": "jest" }, - "dependencies": { - "@logto/core-kit": "1.0.0-beta.20" - }, "devDependencies": { + "@logto/core-kit": "1.0.0-beta.20", "@logto/language-kit": "1.0.0-beta.20", "@logto/phrases": "workspace:^", "@logto/phrases-ui": "workspace:^", diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 84b6a4269..debb58fd5 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -13,6 +13,7 @@ import ForgotPassword from './pages/ForgotPassword'; import Passcode from './pages/Passcode'; import Register from './pages/Register'; import ResetPassword from './pages/ResetPassword'; +import SecondaryRegister from './pages/SecondaryRegister'; import SecondarySignIn from './pages/SecondarySignIn'; import SignIn from './pages/SignIn'; import SocialLanding from './pages/SocialLanding'; @@ -67,7 +68,7 @@ const App = () => { {/* register */} } /> - } /> + } /> {/* forgot password */} } /> diff --git a/packages/ui/src/components/TextLink/index.tsx b/packages/ui/src/components/TextLink/index.tsx index eecfaeadc..3ecf38e2a 100644 --- a/packages/ui/src/components/TextLink/index.tsx +++ b/packages/ui/src/components/TextLink/index.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import type { ReactNode, AnchorHTMLAttributes } from 'react'; import type { TFuncKey } from 'react-i18next'; import { useTranslation } from 'react-i18next'; +import type { LinkProps } from 'react-router-dom'; import { Link } from 'react-router-dom'; import * as styles from './index.module.scss'; @@ -11,15 +12,14 @@ export type Props = AnchorHTMLAttributes & { children?: ReactNode; text?: TFuncKey; type?: 'primary' | 'secondary'; - to?: string; -}; +} & Partial; const TextLink = ({ className, children, text, type = 'primary', to, ...rest }: Props) => { const { t } = useTranslation(); if (to) { return ( - + {children ?? (text ? t(text) : '')} ); diff --git a/packages/ui/src/containers/EmailPassword/index.tsx b/packages/ui/src/containers/EmailPassword/index.tsx new file mode 100644 index 000000000..03cfdde08 --- /dev/null +++ b/packages/ui/src/containers/EmailPassword/index.tsx @@ -0,0 +1,9 @@ +type Props = { + className?: string; +}; + +const EmailPassword = ({ className }: Props) => { + return
email password form
; +}; + +export default EmailPassword; diff --git a/packages/ui/src/containers/LandingPageContainer/index.module.scss b/packages/ui/src/containers/LandingPageContainer/index.module.scss new file mode 100644 index 000000000..467bd2fd2 --- /dev/null +++ b/packages/ui/src/containers/LandingPageContainer/index.module.scss @@ -0,0 +1,28 @@ +@use '@/scss/underscore' as _; + +.wrapper { + @include _.full-page; + @include _.flex-column(normal, normal); + @include _.full-width; +} + +:global(body.mobile) { + .header { + margin-top: _.unit(3); + margin-bottom: _.unit(7); + } +} + +:global(body.desktop) { + .header { + margin-bottom: _.unit(6); + } + + .placeholderTop { + flex: 3; + } + + .placeholderBottom { + flex: 5; + } +} diff --git a/packages/ui/src/containers/LandingPageContainer/index.tsx b/packages/ui/src/containers/LandingPageContainer/index.tsx new file mode 100644 index 000000000..1576e2085 --- /dev/null +++ b/packages/ui/src/containers/LandingPageContainer/index.tsx @@ -0,0 +1,44 @@ +import { BrandingStyle } from '@logto/schemas'; +import classNames from 'classnames'; +import type { ReactNode } from 'react'; +import { useContext } from 'react'; + +import BrandingHeader from '@/components/BrandingHeader'; +import AppNotification from '@/containers/AppNotification'; +import { PageContext } from '@/hooks/use-page-context'; +import { getLogoUrl } from '@/utils/logo'; + +import * as styles from './index.module.scss'; + +type Props = { + children: ReactNode; + className?: string; +}; + +const LandingPageContainer = ({ children, className }: Props) => { + const { experienceSettings, theme, platform } = useContext(PageContext); + + if (!experienceSettings) { + return null; + } + + const { slogan, logoUrl, darkLogoUrl, style } = experienceSettings.branding; + + return ( + <> + {platform === 'web' &&
} +
+ + {children} + +
+ {platform === 'web' &&
} + + ); +}; + +export default LandingPageContainer; diff --git a/packages/ui/src/containers/Passwordless/PasswordlessSwitch.test.tsx b/packages/ui/src/containers/Passwordless/PasswordlessSwitch.test.tsx index a1970f054..12e17bb20 100644 --- a/packages/ui/src/containers/Passwordless/PasswordlessSwitch.test.tsx +++ b/packages/ui/src/containers/Passwordless/PasswordlessSwitch.test.tsx @@ -1,52 +1,29 @@ -import { fireEvent } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import renderWithPageContext from '@/__mocks__/RenderWithPageContext'; import PasswordlessSwitch from './PasswordlessSwitch'; -const mockedNavigate = jest.fn(); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), - useNavigate: () => mockedNavigate, -})); - describe('', () => { - afterEach(() => { - mockedNavigate.mockClear(); - }); - test('render sms passwordless switch', () => { - const { queryByText, getByText } = renderWithPageContext( + const { queryByText, container } = renderWithPageContext( ); expect(queryByText('action.switch_to')).not.toBeNull(); - - const link = getByText('action.switch_to'); - fireEvent.click(link); - - expect(mockedNavigate).toBeCalledWith( - { pathname: '/forgot-password/email' }, - { replace: true } - ); + expect(container.querySelector('a')?.getAttribute('href')).toBe('/forgot-password/email'); }); test('render email passwordless switch', () => { - const { queryByText, getByText } = renderWithPageContext( + const { queryByText, container } = renderWithPageContext( ); expect(queryByText('action.switch_to')).not.toBeNull(); - - const link = getByText('action.switch_to'); - fireEvent.click(link); - - expect(mockedNavigate).toBeCalledWith({ pathname: '/forgot-password/sms' }, { replace: true }); + expect(container.querySelector('a')?.getAttribute('href')).toBe('/forgot-password/sms'); }); }); diff --git a/packages/ui/src/containers/Passwordless/PasswordlessSwitch.tsx b/packages/ui/src/containers/Passwordless/PasswordlessSwitch.tsx index d2a05ef42..592f8fb8b 100644 --- a/packages/ui/src/containers/Passwordless/PasswordlessSwitch.tsx +++ b/packages/ui/src/containers/Passwordless/PasswordlessSwitch.tsx @@ -16,17 +16,7 @@ const PasswordlessSwitch = ({ target, className }: Props) => { const targetPathname = pathname.replace(target === 'email' ? 'sms' : 'email', target); return ( - { - navigate( - { - pathname: targetPathname, - }, - { replace: true } - ); - }} - > + {t('action.switch_to', { method: t(`description.${target === 'email' ? 'email' : 'phone_number'}`), })} diff --git a/packages/ui/src/containers/PhonePassword/index.tsx b/packages/ui/src/containers/PhonePassword/index.tsx new file mode 100644 index 000000000..070004683 --- /dev/null +++ b/packages/ui/src/containers/PhonePassword/index.tsx @@ -0,0 +1,9 @@ +type Props = { + className?: string; +}; + +const PhonePassword = ({ className }: Props) => { + return
Phone password form
; +}; + +export default PhonePassword; diff --git a/packages/ui/src/containers/SignInMethodsLink/index.tsx b/packages/ui/src/containers/SignInMethodsLink/index.tsx index 5c0062a73..8e04d4487 100644 --- a/packages/ui/src/containers/SignInMethodsLink/index.tsx +++ b/packages/ui/src/containers/SignInMethodsLink/index.tsx @@ -1,51 +1,47 @@ +import type { SignIn } from '@logto/schemas'; +import { SignInIdentifier } from '@logto/schemas'; import classNames from 'classnames'; import type { ReactNode } from 'react'; import { useMemo } from 'react'; import type { TFuncKey } from 'react-i18next'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; import reactStringReplace from 'react-string-replace'; import TextLink from '@/components/TextLink'; -import type { LocalSignInMethod } from '@/types'; import * as styles from './index.module.scss'; type Props = { - signInMethods: LocalSignInMethod[]; + signInMethods: SignIn['methods']; + // Allows social page to pass additional query params to the sign-in pages search?: string; className?: string; template?: TFuncKey<'translation', 'secondary'>; }; const SignInMethodsKeyMap: { - [key in LocalSignInMethod]: TFuncKey<'translation', 'input'>; + [key in SignInIdentifier]: TFuncKey<'translation', 'input'>; } = { - username: 'username', - email: 'email', - sms: 'phone_number', + [SignInIdentifier.Username]: 'username', + [SignInIdentifier.Email]: 'email', + [SignInIdentifier.Sms]: 'phone_number', }; const SignInMethodsLink = ({ signInMethods, template, search, className }: Props) => { - const navigate = useNavigate(); const { t } = useTranslation(); + const identifiers = signInMethods.map(({ identifier }) => identifier); const signInMethodsLink = useMemo( () => - signInMethods.map((method) => ( + identifiers.map((identifier) => ( { - navigate({ - pathname: `/sign-in/${method}`, - search, - }); - }} + text={`input.${SignInMethodsKeyMap[identifier]}`} + to={{ pathname: `/sign-in/${identifier}`, search }} /> )), - [navigate, search, signInMethods] + [identifiers, search] ); if (signInMethodsLink.length === 0) { @@ -58,11 +54,11 @@ const SignInMethodsLink = ({ signInMethods, template, search, className }: Props } // With text template - const rawText = t(`secondary.${template}`, { methods: signInMethods }); - const textWithLink: ReactNode = signInMethods.reduce( - (content, method, index) => + const rawText = t(`secondary.${template}`, { methods: identifiers }); + const textWithLink: ReactNode = identifiers.reduce( + (content, identifier, index) => // @ts-expect-error: reactStringReplace type bug, using deprecated ReactNodeArray as its input type - reactStringReplace(content, method, () => signInMethodsLink[index]), + reactStringReplace(content, identifier, () => signInMethodsLink[index]), rawText ); diff --git a/packages/ui/src/containers/SocialCreateAccount/index.tsx b/packages/ui/src/containers/SocialCreateAccount/index.tsx index 6224c3f69..455a290d0 100644 --- a/packages/ui/src/containers/SocialCreateAccount/index.tsx +++ b/packages/ui/src/containers/SocialCreateAccount/index.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import Button from '@/components/Button'; import useBindSocial from '@/hooks/use-bind-social'; +import { useSieMethods } from '@/hooks/use-sie'; import { SearchParameters } from '@/types'; import { queryStringify } from '@/utils'; @@ -16,8 +17,8 @@ type Props = { const SocialCreateAccount = ({ connectorId, className }: Props) => { const { t } = useTranslation(); - const { relatedUser, localSignInMethods, registerWithSocial, bindSocialRelatedUser } = - useBindSocial(); + const { relatedUser, registerWithSocial, bindSocialRelatedUser } = useBindSocial(); + const { signInMethods } = useSieMethods(); return (
@@ -42,7 +43,7 @@ const SocialCreateAccount = ({ connectorId, className }: Props) => { }} /> { ); - expect(container.querySelectorAll('button')).toHaveLength(defaultSize); + expect(container.querySelectorAll('button')).toHaveLength( + socialConnectors.slice(0, defaultSize).length + ); }); it('more than three connectors', () => { const { container } = renderWithPageContext( - + ); diff --git a/packages/ui/src/containers/SocialSignIn/SocialSignInList/index.tsx b/packages/ui/src/containers/SocialSignIn/SocialSignInList/index.tsx index a86cc3d25..de44760e6 100644 --- a/packages/ui/src/containers/SocialSignIn/SocialSignInList/index.tsx +++ b/packages/ui/src/containers/SocialSignIn/SocialSignInList/index.tsx @@ -16,15 +16,9 @@ type Props = { className?: string; socialConnectors?: ConnectorMetadata[]; isCollapseEnabled?: boolean; - onSocialSignInCallback?: () => void; }; -const SocialSignInList = ({ - className, - socialConnectors = [], - isCollapseEnabled = true, - onSocialSignInCallback, -}: Props) => { +const SocialSignInList = ({ className, socialConnectors = [], isCollapseEnabled }: Props) => { const [expand, setExpand] = useState(false); const { invokeSocialSignIn, theme } = useSocial(); const isOverSize = socialConnectors.length > defaultSize; @@ -52,7 +46,6 @@ const SocialSignInList = ({ target={target} onClick={() => { void invokeSocialSignIn(connector); - onSocialSignInCallback?.(); }} /> ); diff --git a/packages/ui/src/containers/SocialSignIn/index.tsx b/packages/ui/src/containers/SocialSignIn/index.tsx index 90463b006..1f2b01ffc 100644 --- a/packages/ui/src/containers/SocialSignIn/index.tsx +++ b/packages/ui/src/containers/SocialSignIn/index.tsx @@ -5,8 +5,6 @@ import useSocial from '@/hooks/use-social'; import SocialSignInList from './SocialSignInList'; import * as styles from './index.module.scss'; -export const defaultSize = 3; - type Props = { className?: string; }; @@ -24,3 +22,5 @@ const SocialSignIn = ({ className }: Props) => { }; export default SocialSignIn; + +export { default as SocialSignInList } from './SocialSignInList'; diff --git a/packages/ui/src/hooks/use-bind-social.ts b/packages/ui/src/hooks/use-bind-social.ts index a638d49b6..2d1987248 100644 --- a/packages/ui/src/hooks/use-bind-social.ts +++ b/packages/ui/src/hooks/use-bind-social.ts @@ -1,16 +1,14 @@ import { conditional } from '@silverhand/essentials'; -import { useCallback, useEffect, useContext, useMemo } from 'react'; +import { useCallback, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; import { is } from 'superstruct'; import { registerWithSocial, bindSocialRelatedUser } from '@/apis/social'; import useApi from '@/hooks/use-api'; -import { PageContext } from '@/hooks/use-page-context'; import { bindSocialStateGuard } from '@/types/guard'; const useBindSocial = () => { const { state } = useLocation(); - const { experienceSettings } = useContext(PageContext); const { result: registerResult, run: asyncRegisterWithSocial } = useApi(registerWithSocial); const { result: bindUserResult, run: asyncBindSocialRelatedUser } = useApi(bindSocialRelatedUser); @@ -28,13 +26,6 @@ const useBindSocial = () => { [asyncBindSocialRelatedUser] ); - // TODO: @simeng LOG-4487 - const localSignInMethods = useMemo(() => { - const signInMethods = experienceSettings?.signIn.methods ?? []; - - return signInMethods.map(({ identifier }) => identifier); - }, [experienceSettings]); - useEffect(() => { if (registerResult?.redirectTo) { window.location.replace(registerResult.redirectTo); @@ -48,7 +39,6 @@ const useBindSocial = () => { }, [bindUserResult]); return { - localSignInMethods, relatedUser: conditional(is(state, bindSocialStateGuard) && state.relatedUser), registerWithSocial: createAccountHandler, bindSocialRelatedUser: bindRelatedUserHandler, diff --git a/packages/ui/src/hooks/use-sie.ts b/packages/ui/src/hooks/use-sie.ts new file mode 100644 index 000000000..79fb0f8c1 --- /dev/null +++ b/packages/ui/src/hooks/use-sie.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react'; + +import { PageContext } from './use-page-context'; + +export const useSieMethods = () => { + const { experienceSettings } = useContext(PageContext); + + return { + signUpMethods: experienceSettings?.signUp.methods ?? [], + signInMethods: experienceSettings?.signIn.methods ?? [], + socialConnectors: experienceSettings?.socialConnectors ?? [], + }; +}; diff --git a/packages/ui/src/pages/Register/index.tsx b/packages/ui/src/pages/Register/index.tsx index 81317781d..349115c10 100644 --- a/packages/ui/src/pages/Register/index.tsx +++ b/packages/ui/src/pages/Register/index.tsx @@ -1,50 +1,16 @@ -import { useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; +import { useContext } from 'react'; -import NavBar from '@/components/NavBar'; -import CreateAccount from '@/containers/CreateAccount'; -import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless'; -import ErrorPage from '@/pages/ErrorPage'; - -import * as styles from './index.module.scss'; - -type Parameters = { - method?: string; -}; +import LandingPageContainer from '@/containers/LandingPageContainer'; +import { PageContext } from '@/hooks/use-page-context'; const Register = () => { - const { t } = useTranslation(); - const { method = 'username' } = useParams(); + const { experienceSettings } = useContext(PageContext); - const registerForm = useMemo(() => { - if (method === 'sms') { - // eslint-disable-next-line jsx-a11y/no-autofocus - return ; - } - - if (method === 'email') { - // eslint-disable-next-line jsx-a11y/no-autofocus - return ; - } - - // eslint-disable-next-line jsx-a11y/no-autofocus - return ; - }, [method]); - - if (!['email', 'sms', 'username'].includes(method)) { - return ; + if (!experienceSettings) { + return null; } - return ( -
- -
-
{t('action.create_account')}
- {registerForm} -
-
- ); + return signUp; }; export default Register; diff --git a/packages/ui/src/pages/Register/index.module.scss b/packages/ui/src/pages/SecondaryRegister/index.module.scss similarity index 100% rename from packages/ui/src/pages/Register/index.module.scss rename to packages/ui/src/pages/SecondaryRegister/index.module.scss diff --git a/packages/ui/src/pages/Register/index.test.tsx b/packages/ui/src/pages/SecondaryRegister/index.test.tsx similarity index 81% rename from packages/ui/src/pages/Register/index.test.tsx rename to packages/ui/src/pages/SecondaryRegister/index.test.tsx index d413b8e22..ff8f31b72 100644 --- a/packages/ui/src/pages/Register/index.test.tsx +++ b/packages/ui/src/pages/SecondaryRegister/index.test.tsx @@ -1,18 +1,18 @@ import { render } from '@testing-library/react'; import { Routes, Route, MemoryRouter } from 'react-router-dom'; -import Register from '@/pages/Register'; +import SecondaryRegister from '@/pages/SecondaryRegister'; jest.mock('@/apis/register', () => ({ register: jest.fn(async () => 0) })); jest.mock('i18next', () => ({ language: 'en', })); -describe('', () => { +describe('', () => { test('renders without exploding', async () => { const { queryByText } = render( - + ); expect(queryByText('action.create_account')).not.toBeNull(); @@ -23,7 +23,7 @@ describe('', () => { const { queryByText, container } = render( - } /> + } /> ); @@ -35,7 +35,7 @@ describe('', () => { const { queryByText, container } = render( - } /> + } /> ); @@ -47,7 +47,7 @@ describe('', () => { const { queryByText } = render( - } /> + } /> ); diff --git a/packages/ui/src/pages/SecondaryRegister/index.tsx b/packages/ui/src/pages/SecondaryRegister/index.tsx new file mode 100644 index 000000000..e7a5a4768 --- /dev/null +++ b/packages/ui/src/pages/SecondaryRegister/index.tsx @@ -0,0 +1,50 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; + +import NavBar from '@/components/NavBar'; +import CreateAccount from '@/containers/CreateAccount'; +import { PhonePasswordless, EmailPasswordless } from '@/containers/Passwordless'; +import ErrorPage from '@/pages/ErrorPage'; + +import * as styles from './index.module.scss'; + +type Parameters = { + method?: string; +}; + +const SecondaryRegister = () => { + const { t } = useTranslation(); + const { method = 'username' } = useParams(); + + const registerForm = useMemo(() => { + if (method === 'sms') { + // eslint-disable-next-line jsx-a11y/no-autofocus + return ; + } + + if (method === 'email') { + // eslint-disable-next-line jsx-a11y/no-autofocus + return ; + } + + // eslint-disable-next-line jsx-a11y/no-autofocus + return ; + }, [method]); + + if (!['email', 'sms', 'username'].includes(method)) { + return ; + } + + return ( +
+ +
+
{t('action.create_account')}
+ {registerForm} +
+
+ ); +}; + +export default SecondaryRegister; diff --git a/packages/ui/src/pages/SignIn/Main.tsx b/packages/ui/src/pages/SignIn/Main.tsx new file mode 100644 index 000000000..794dcbcc5 --- /dev/null +++ b/packages/ui/src/pages/SignIn/Main.tsx @@ -0,0 +1,53 @@ +import type { SignIn as SignInType, ConnectorMetadata } from '@logto/schemas'; + +import EmailPassword from '@/containers/EmailPassword'; +import { EmailPasswordless, PhonePasswordless } from '@/containers/Passwordless'; +import PhonePassword from '@/containers/PhonePassword'; +import SocialSignIn from '@/containers/SocialSignIn'; +import UsernameSignIn from '@/containers/UsernameSignIn'; +import type { ArrayElement } from '@/types'; + +import * as styles from './index.module.scss'; + +type Props = { + signInMethod?: ArrayElement; + socialConnectors: ConnectorMetadata[]; +}; + +const Main = ({ signInMethod, socialConnectors }: Props) => { + if (!signInMethod) { + return socialConnectors.length > 0 ? : null; + } + + switch (signInMethod.identifier) { + case 'email': { + if (signInMethod.password && !signInMethod.verificationCode) { + return ; + } + + return ; + } + + case 'sms': { + if (signInMethod.password && !signInMethod.verificationCode) { + return ; + } + + return ; + } + + case 'username': { + return ; + } + + default: { + if (socialConnectors.length > 0) { + return ; + } + + return null; + } + } +}; + +export default Main; diff --git a/packages/ui/src/pages/SignIn/MainForm.tsx b/packages/ui/src/pages/SignIn/MainForm.tsx deleted file mode 100644 index f4c51a9c4..000000000 --- a/packages/ui/src/pages/SignIn/MainForm.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useContext } from 'react'; - -import { EmailPasswordless, PhonePasswordless } from '@/containers/Passwordless'; -import SocialSignIn from '@/containers/SocialSignIn'; -import UsernameSignIn from '@/containers/UsernameSignIn'; -import { PageContext } from '@/hooks/use-page-context'; - -import * as styles from './index.module.scss'; - -const MainForm = () => { - const { experienceSettings } = useContext(PageContext); - - if (!experienceSettings) { - return null; - } - - const { signIn, socialConnectors } = experienceSettings; - const primarySignInMethod = signIn.methods[0]; - - switch (primarySignInMethod?.identifier) { - case 'email': - return ; - case 'sms': - return ; - case 'username': - return ; - - default: { - if (socialConnectors.length > 0) { - return ; - } - - return null; - } - } -}; - -export default MainForm; diff --git a/packages/ui/src/pages/SignIn/index.module.scss b/packages/ui/src/pages/SignIn/index.module.scss index 66b5c3a40..33e8bdab4 100644 --- a/packages/ui/src/pages/SignIn/index.module.scss +++ b/packages/ui/src/pages/SignIn/index.module.scss @@ -1,46 +1,24 @@ @use '@/scss/underscore' as _; -.wrapper { - @include _.full-page; - @include _.flex-column(normal, normal); - @include _.full-width; - - .primarySignIn { - margin-bottom: _.unit(5); - } - - .otherMethodsLink { - margin-bottom: _.unit(6); - } - - .createAccount { - margin-top: _.unit(6); - text-align: center; - } - - .placeHolder { - flex: 1; - } +.main { + margin-bottom: _.unit(4); } -.placeholderTop { - flex: 3; +.otherMethodsLink { + margin-bottom: _.unit(6); } -.placeholderBottom { - flex: 5; +.createAccount { + margin-top: _.unit(6); + text-align: center; } +.placeHolder { + flex: 1; +} + + :global(body.mobile) { - .header { - margin-top: _.unit(3); - margin-bottom: _.unit(12); - } - - .primarySocial { - margin-bottom: _.unit(8); - } - .divider { margin-bottom: _.unit(5); } @@ -51,14 +29,10 @@ } :global(body.desktop) { - .header { + .main { margin-bottom: _.unit(6); } - .primarySocial { - margin-bottom: _.unit(12); - } - .placeHolder { flex: 0; } diff --git a/packages/ui/src/pages/SignIn/index.test.tsx b/packages/ui/src/pages/SignIn/index.test.tsx index 50c95ebae..2ea66e266 100644 --- a/packages/ui/src/pages/SignIn/index.test.tsx +++ b/packages/ui/src/pages/SignIn/index.test.tsx @@ -16,18 +16,25 @@ jest.mock('i18next', () => ({ describe('', () => { test('renders with username as primary', async () => { - const { queryByText, container } = renderWithPageContext( + const { queryByText, queryAllByText, container } = renderWithPageContext( ); + expect(container.querySelector('input[name="username"]')).not.toBeNull(); expect(queryByText('action.sign_in')).not.toBeNull(); + + // Other sign-in methods + expect(queryByText('secondary.sign_in_with')).not.toBeNull(); + + // Social + expect(queryAllByText('action.sign_in_with')).toHaveLength(defaultSize); }); - test('renders with email as primary', async () => { + test('renders with email passwordless as primary', async () => { const { queryByText, container } = renderWithPageContext( ', () => { expect(queryByText('action.continue')).not.toBeNull(); }); - test('renders with sms as primary', async () => { + test('render with email password as primary', async () => { + const { queryByText } = renderWithPageContext( + + + + + + ); + expect(queryByText('email password form')).not.toBeNull(); + }); + + test('renders with sms passwordless as primary', async () => { const { queryByText, container } = renderWithPageContext( ', () => { expect(queryByText('action.continue')).not.toBeNull(); }); + test('renders with phone password as primary', async () => { + const { queryByText, container } = renderWithPageContext( + + + + + + ); + expect(queryByText('Phone password form')).not.toBeNull(); + }); + test('renders with social as primary', async () => { const { container } = renderWithPageContext( @@ -67,6 +122,8 @@ describe('', () => { ); - expect(container.querySelectorAll('button')).toHaveLength(defaultSize + 1); // Plus Expand Button + expect(container.querySelectorAll('button')).toHaveLength( + mockSignInExperienceSettings.socialConnectors.length + ); }); }); diff --git a/packages/ui/src/pages/SignIn/index.tsx b/packages/ui/src/pages/SignIn/index.tsx index 50f41eb38..abb35e3b3 100644 --- a/packages/ui/src/pages/SignIn/index.tsx +++ b/packages/ui/src/pages/SignIn/index.tsx @@ -1,38 +1,50 @@ -import { BrandingStyle } from '@logto/schemas'; -import classNames from 'classnames'; -import { useContext } from 'react'; +import Divider from '@/components/Divider'; +import TextLink from '@/components/TextLink'; +import LandingPageContainer from '@/containers/LandingPageContainer'; +import SignInMethodsLink from '@/containers/SignInMethodsLink'; +import { SocialSignInList } from '@/containers/SocialSignIn'; +import { useSieMethods } from '@/hooks/use-sie'; -import BrandingHeader from '@/components/BrandingHeader'; -import AppNotification from '@/containers/AppNotification'; -import { PageContext } from '@/hooks/use-page-context'; -import { getLogoUrl } from '@/utils/logo'; - -import MainForm from './MainForm'; +import Main from './Main'; import * as styles from './index.module.scss'; const SignIn = () => { - const { experienceSettings, theme, platform } = useContext(PageContext); - - if (!experienceSettings) { - return null; - } - - const { slogan, logoUrl, darkLogoUrl, style } = experienceSettings.branding; + const { signInMethods, signUpMethods, socialConnectors } = useSieMethods(); + const otherMethods = signInMethods.slice(1); return ( - <> - {platform === 'web' &&
} -
- - - -
- {platform === 'web' &&
} - + +
+ { + // Other sign-in methods + otherMethods.length > 0 && ( + + ) + } + { + // Social sign-in methods + signInMethods.length > 0 && socialConnectors.length > 0 && ( + <> + + + + ) + } + { + // Create Account footer + signUpMethods.length > 0 && ( + <> +
+ + + ) + } + ); }; diff --git a/packages/ui/src/types/index.ts b/packages/ui/src/types/index.ts index f90cfe9fd..4ce2c9dc9 100644 --- a/packages/ui/src/types/index.ts +++ b/packages/ui/src/types/index.ts @@ -47,3 +47,9 @@ export type PreviewConfig = { platform: Platform; isNative: boolean; }; + +export type ArrayElement = ArrayType extends ReadonlyArray< + infer ElementType +> + ? ElementType + : never; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8523e82fd..9428eea10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -707,9 +707,8 @@ importers: superstruct: ^0.16.0 typescript: ^4.7.4 use-debounced-loader: ^0.1.1 - dependencies: - '@logto/core-kit': 1.0.0-beta.20 devDependencies: + '@logto/core-kit': 1.0.0-beta.20 '@logto/language-kit': 1.0.0-beta.20 '@logto/phrases': link:../phrases '@logto/phrases-ui': link:../phrases-ui @@ -7494,7 +7493,7 @@ packages: /history/5.3.0: resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==} dependencies: - '@babel/runtime': 7.18.3 + '@babel/runtime': 7.19.4 dev: true /hoist-non-react-statics/3.3.2: