-
-
Notifications
You must be signed in to change notification settings - Fork 461
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(experience): add identifier sign-in page
- Loading branch information
Showing
28 changed files
with
337 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
packages/experience/src/Layout/FirstScreenLayout/index.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
@use '@/scss/underscore' as _; | ||
|
||
.wrapper { | ||
@include _.full-page; | ||
@include _.flex-column(normal, normal); | ||
@include _.full-width; | ||
|
||
> *:last-child { | ||
margin-bottom: 0; | ||
} | ||
} | ||
|
||
:global(body.desktop) { | ||
.wrapper { | ||
padding: _.unit(6) 0; | ||
} | ||
|
||
.placeholderTop { | ||
flex: 3; | ||
} | ||
|
||
.placeholderBottom { | ||
flex: 5; | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
packages/experience/src/Layout/FirstScreenLayout/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { type ReactNode, useContext } from 'react'; | ||
|
||
import PageContext from '@/Providers/PageContextProvider/PageContext'; | ||
|
||
import PageMeta from '../../components/PageMeta'; | ||
import type { Props as PageMetaProps } from '../../components/PageMeta'; | ||
|
||
import styles from './index.module.scss'; | ||
|
||
type Props = { | ||
readonly children: ReactNode; | ||
readonly pageMeta: PageMetaProps; | ||
}; | ||
|
||
const FirstScreenLayout = ({ children, pageMeta }: Props) => { | ||
const { platform } = useContext(PageContext); | ||
|
||
return ( | ||
<> | ||
<PageMeta {...pageMeta} /> | ||
{platform === 'web' && <div className={styles.placeholderTop} />} | ||
<div className={styles.wrapper}>{children}</div> | ||
{platform === 'web' && <div className={styles.placeholderBottom} />} | ||
</> | ||
); | ||
}; | ||
|
||
export default FirstScreenLayout; |
33 changes: 33 additions & 0 deletions
33
packages/experience/src/Layout/IdentifierPageLayout/index.module.scss
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
@use '@/scss/underscore' as _; | ||
|
||
.header { | ||
margin: _.unit(6) 0; | ||
} | ||
|
||
.description { | ||
margin-top: _.unit(2); | ||
@include _.text-hint; | ||
} | ||
|
||
.terms { | ||
margin-top: _.unit(4); | ||
@include _.text-hint; | ||
text-align: center; | ||
font: var(--font-body-3); | ||
} | ||
|
||
.link { | ||
margin-top: _.unit(7); | ||
} | ||
|
||
:global(body.mobile) { | ||
.title { | ||
@include _.title; | ||
} | ||
} | ||
|
||
:global(body.desktop) { | ||
.title { | ||
@include _.title_desktop; | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
packages/experience/src/Layout/IdentifierPageLayout/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { type AgreeToTermsPolicy } from '@logto/schemas'; | ||
import { type TFuncKey } from 'i18next'; | ||
import { useMemo, type ReactNode } from 'react'; | ||
|
||
import DynamicT from '@/components/DynamicT'; | ||
import type { Props as PageMetaProps } from '@/components/PageMeta'; | ||
import type { Props as TextLinkProps } from '@/components/TextLink'; | ||
import TextLink from '@/components/TextLink'; | ||
import TermsAndPrivacyLinks from '@/containers/TermsAndPrivacyLinks'; | ||
import useTerms from '@/hooks/use-terms'; | ||
|
||
import FirstScreenLayout from '../FirstScreenLayout'; | ||
|
||
import styles from './index.module.scss'; | ||
|
||
type Props = { | ||
readonly children: ReactNode; | ||
readonly pageMeta: PageMetaProps; | ||
readonly title: TFuncKey; | ||
readonly description: string; | ||
readonly footerTermsDisplayPolicies: AgreeToTermsPolicy[]; | ||
readonly authOptionsLink: TextLinkProps; | ||
}; | ||
|
||
const IdentifierPageLayout = ({ | ||
children, | ||
pageMeta, | ||
title, | ||
description, | ||
footerTermsDisplayPolicies, | ||
authOptionsLink, | ||
}: Props) => { | ||
const { agreeToTermsPolicy } = useTerms(); | ||
|
||
const shouldDisplayFooterTerms = useMemo( | ||
() => agreeToTermsPolicy && footerTermsDisplayPolicies.includes(agreeToTermsPolicy), | ||
[agreeToTermsPolicy, footerTermsDisplayPolicies] | ||
); | ||
|
||
return ( | ||
<FirstScreenLayout pageMeta={pageMeta}> | ||
<div className={styles.header}> | ||
<div className={styles.title}> | ||
<DynamicT forKey={title} /> | ||
</div> | ||
<div className={styles.description}>{description}</div> | ||
</div> | ||
{children} | ||
{shouldDisplayFooterTerms && <TermsAndPrivacyLinks className={styles.terms} />} | ||
<TextLink {...authOptionsLink} className={styles.link} /> | ||
</FirstScreenLayout> | ||
); | ||
}; | ||
|
||
export default IdentifierPageLayout; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { yes } from '@silverhand/essentials'; | ||
|
||
export const isDevFeaturesEnabled = | ||
process.env.NODE_ENV !== 'production' || yes(process.env.DEV_FEATURES_ENABLED); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { useSearchParams } from 'react-router-dom'; | ||
|
||
import { identifierSearchParamGuard } from '@/types/guard'; | ||
/** | ||
* Extracts and validates sign-in identifiers from URL search parameters. | ||
* | ||
* Functionality: | ||
* 1. Extracts all 'identifier' values from the URL search parameters. | ||
* 2. Validates these values to ensure they are valid `SignInIdentifier`. | ||
* 3. Returns an array of validated sign-in identifiers. | ||
*/ | ||
const useIdentifierParams = () => { | ||
const [searchParams] = useSearchParams(); | ||
|
||
// Todo @xiaoyijun use a constant for the key | ||
Check warning on line 15 in packages/experience/src/hooks/use-identifier-params.ts GitHub Actions / ESLint Report Analysispackages/experience/src/hooks/use-identifier-params.ts#L15
|
||
const rawIdentifiers = searchParams.getAll('identifier'); | ||
const [_, identifiers = []] = identifierSearchParamGuard.validate(rawIdentifiers); | ||
|
||
return { identifiers }; | ||
}; | ||
|
||
export default useIdentifierParams; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { AgreeToTermsPolicy, experience } from '@logto/schemas'; | ||
import { useMemo } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Navigate } from 'react-router-dom'; | ||
|
||
import IdentifierPageLayout from '@/Layout/IdentifierPageLayout'; | ||
import { identifierInputDescriptionMap } from '@/utils/form'; | ||
|
||
import IdentifierSignInForm from '../SignIn/IdentifierSignInForm'; | ||
import PasswordSignInForm from '../SignIn/PasswordSignInForm'; | ||
|
||
import useIdentifierSignInMethods from './use-identifier-sign-in-methods'; | ||
|
||
const IdentifierSignIn = () => { | ||
const { t } = useTranslation(); | ||
|
||
const signInMethods = useIdentifierSignInMethods(); | ||
|
||
const isPasswordOnly = useMemo( | ||
() => | ||
signInMethods.length > 0 && | ||
signInMethods.every(({ password, verificationCode }) => password && !verificationCode), | ||
[signInMethods] | ||
); | ||
|
||
// Fallback to sign-in page if no sign-in methods are available | ||
if (signInMethods.length === 0) { | ||
return <Navigate to={`/${experience.routes.signIn}`} />; | ||
} | ||
|
||
return ( | ||
<IdentifierPageLayout | ||
pageMeta={{ titleKey: 'description.sign_in' }} | ||
title="description.sign_in" | ||
description={t('description.identifier_sign_in_description', { | ||
types: signInMethods.map(({ identifier }) => t(identifierInputDescriptionMap[identifier])), | ||
})} | ||
footerTermsDisplayPolicies={[ | ||
AgreeToTermsPolicy.Automatic, | ||
AgreeToTermsPolicy.ManualRegistrationOnly, | ||
]} | ||
authOptionsLink={{ | ||
to: `/${experience.routes.signIn}`, | ||
text: 'description.all_sign_in_options', | ||
}} | ||
> | ||
{isPasswordOnly ? ( | ||
<PasswordSignInForm signInMethods={signInMethods.map(({ identifier }) => identifier)} /> | ||
) : ( | ||
<IdentifierSignInForm signInMethods={signInMethods} /> | ||
)} | ||
</IdentifierPageLayout> | ||
); | ||
}; | ||
|
||
export default IdentifierSignIn; |
35 changes: 35 additions & 0 deletions
35
packages/experience/src/pages/IdentifierSignIn/use-identifier-sign-in-methods.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { useMemo } from 'react'; | ||
|
||
import useIdentifierParams from '@/hooks/use-identifier-params'; | ||
import { useSieMethods } from '@/hooks/use-sie'; | ||
|
||
/** | ||
* Read sign-in methods from sign-in experience config and URL identifier parameters. | ||
* | ||
* Sign-in methods fallback logic: | ||
* 1. If no identifiers are provided in the URL, return all sign-in methods from sign-in experience config. | ||
* 2. If identifiers are provided in the URL but all of them are not supported by the sign-in experience config, return all sign-in methods from sign-in experience config. | ||
* 3. If identifiers are provided in the URL and supported by the sign-in experience config, return the intersection of the two. | ||
*/ | ||
const useIdentifierSignInMethods = () => { | ||
const { signInMethods } = useSieMethods(); | ||
const { identifiers } = useIdentifierParams(); | ||
|
||
return useMemo(() => { | ||
// Fallback to all sign-in methods if no identifiers are provided | ||
if (identifiers.length === 0) { | ||
return signInMethods; | ||
} | ||
|
||
const methods = signInMethods.filter(({ identifier }) => identifiers.includes(identifier)); | ||
|
||
// Fallback to all sign-in methods if no identifiers are supported | ||
if (methods.length === 0) { | ||
return signInMethods; | ||
} | ||
|
||
return methods; | ||
}, [identifiers, signInMethods]); | ||
}; | ||
|
||
export default useIdentifierSignInMethods; |
Oops, something went wrong.