Skip to content

Commit

Permalink
feat(experience): support agree to terms policies
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoyijun committed Jun 18, 2024
1 parent 5eaa6e3 commit 0595e3e
Show file tree
Hide file tree
Showing 30 changed files with 259 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ConnectorPlatform, type ConnectorMetadata } from '@logto/schemas';
import { AgreeToTermsPolicy, ConnectorPlatform, type ConnectorMetadata } from '@logto/schemas';
import { useCallback, useContext } from 'react';

import PageContext from '@/Providers/PageContextProvider/PageContext';
import { getSocialAuthorizationUrl } from '@/apis/interaction';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';
import useTerms from '@/hooks/use-terms';
import { getLogtoNativeSdk, isNativeWebview } from '@/utils/native-sdk';
import { generateState, storeState, buildSocialLandingUri } from '@/utils/social-connectors';

Expand All @@ -13,6 +14,7 @@ const useSocial = () => {

const handleError = useErrorHandler();
const asyncInvokeSocialSignIn = useApi(getSocialAuthorizationUrl);
const { termsValidation, agreeToTermsPolicy } = useTerms();

const nativeSignInHandler = useCallback((redirectTo: string, connector: ConnectorMetadata) => {
const { id: connectorId, platform } = connector;
Expand All @@ -30,6 +32,10 @@ const useSocial = () => {

const invokeSocialSignInHandler = useCallback(
async (connector: ConnectorMetadata) => {
if (agreeToTermsPolicy === AgreeToTermsPolicy.Manual && !(await termsValidation())) {
return;
}

const { id: connectorId } = connector;

const state = generateState();
Expand Down Expand Up @@ -61,7 +67,7 @@ const useSocial = () => {
// Invoke web social sign-in flow
window.location.assign(result.redirectTo);
},
[asyncInvokeSocialSignIn, handleError, nativeSignInHandler]
[agreeToTermsPolicy, asyncInvokeSocialSignIn, handleError, nativeSignInHandler, termsValidation]
);

return {
Expand Down
27 changes: 25 additions & 2 deletions packages/experience/src/containers/TermsAndPrivacyLinks/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { AgreeToTermsPolicy } from '@logto/schemas';
import { t } from 'i18next';
import { Trans } from 'react-i18next';

import TermsLinks from '@/components/TermsLinks';
import useTerms from '@/hooks/use-terms';

Expand All @@ -7,15 +11,34 @@ type Props = {

// For sign-in page displaying terms and privacy links use only. No user interaction is needed.
const TermsAndPrivacyLinks = ({ className }: Props) => {
const { termsOfUseUrl, privacyPolicyUrl, isTermsDisabled } = useTerms();
const { termsOfUseUrl, privacyPolicyUrl, isTermsDisabled, agreeToTermsPolicy } = useTerms();

if (isTermsDisabled) {
return null;
}

return (
<div className={className}>
<TermsLinks termsOfUseUrl={termsOfUseUrl} privacyPolicyUrl={privacyPolicyUrl} />
{
// Agreement automatically
agreeToTermsPolicy === AgreeToTermsPolicy.Automatic ? (
<Trans
components={{
link: (
<TermsLinks
inline
termsOfUseUrl={termsOfUseUrl}
privacyPolicyUrl={privacyPolicyUrl}
/>
),
}}
>
{t('description.auto_agreement')}
</Trans>
) : (
<TermsLinks termsOfUseUrl={termsOfUseUrl} privacyPolicyUrl={privacyPolicyUrl} />
)
}
</div>
);
};
Expand Down
11 changes: 7 additions & 4 deletions packages/experience/src/hooks/use-terms.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AgreeToTermsPolicy } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useCallback, useContext, useMemo } from 'react';

Expand All @@ -10,14 +11,15 @@ const useTerms = () => {
const { termsAgreement, setTermsAgreement, experienceSettings } = useContext(PageContext);
const { show } = useConfirmModal();

const { termsOfUseUrl, privacyPolicyUrl, isTermsDisabled } = useMemo(() => {
const { termsOfUseUrl, privacyPolicyUrl } = experienceSettings ?? {};
const { termsOfUseUrl, privacyPolicyUrl, isTermsDisabled, agreeToTermsPolicy } = useMemo(() => {
const { termsOfUseUrl, privacyPolicyUrl, agreeToTermsPolicy } = experienceSettings ?? {};
const isTermsDisabled = !termsOfUseUrl && !privacyPolicyUrl;

return {
termsOfUseUrl: conditional(termsOfUseUrl),
privacyPolicyUrl: conditional(privacyPolicyUrl),
isTermsDisabled,
agreeToTermsPolicy,
};
}, [experienceSettings]);

Expand All @@ -36,18 +38,19 @@ const useTerms = () => {
}, [setTermsAgreement, show]);

const termsValidation = useCallback(async () => {
if (termsAgreement || isTermsDisabled) {
if (termsAgreement || isTermsDisabled || agreeToTermsPolicy === AgreeToTermsPolicy.Automatic) {
return true;
}

return termsAndPrivacyConfirmModalHandler();
}, [termsAgreement, isTermsDisabled, termsAndPrivacyConfirmModalHandler]);
}, [termsAgreement, isTermsDisabled, agreeToTermsPolicy, termsAndPrivacyConfirmModalHandler]);

return {
termsOfUseUrl,
privacyPolicyUrl,
termsAgreement,
isTermsDisabled,
agreeToTermsPolicy,
termsValidation,
setTermsAgreement,
termsAndPrivacyConfirmModalHandler,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SignInIdentifier } from '@logto/schemas';
import { AgreeToTermsPolicy, type SignInIdentifier } from '@logto/schemas';
import classNames from 'classnames';
import { useCallback, useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
Expand Down Expand Up @@ -30,7 +30,7 @@ type FormState = {

const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props) => {
const { t } = useTranslation();
const { termsValidation } = useTerms();
const { termsValidation, agreeToTermsPolicy } = useTerms();

const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit();

Expand Down Expand Up @@ -131,7 +131,11 @@ const IdentifierRegisterForm = ({ className, autoFocus, signUpMethods }: Props)
* If the autofill value is SSO enabled, it will always show SSO form.
*/}
<TermsAndPrivacyCheckbox
className={classNames(styles.terms, showSingleSignOnForm && styles.hidden)}
className={classNames(
styles.terms,
(showSingleSignOnForm || agreeToTermsPolicy === AgreeToTermsPolicy.Automatic) &&
styles.hidden
)}
/>

<Button
Expand Down
7 changes: 7 additions & 0 deletions packages/experience/src/pages/Register/index.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@

.terms {
margin-bottom: _.unit(4);
text-align: center;
@include _.text-hint;
font: var(--font-body-3);
}

.checkbox {
justify-content: center;
}

.createAccount,
Expand Down
49 changes: 39 additions & 10 deletions packages/experience/src/pages/Register/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { SignInMode } from '@logto/schemas';
import { useContext } from 'react';
import { AgreeToTermsPolicy, SignInMode } from '@logto/schemas';
import { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Navigate } from 'react-router-dom';
import { Navigate, useNavigate } from 'react-router-dom';

import LandingPageLayout from '@/Layout/LandingPageLayout';
import SingleSignOnFormModeContextProvider from '@/Providers/SingleSignOnFormModeContextProvider';
Expand All @@ -10,21 +10,32 @@ import Divider from '@/components/Divider';
import TextLink from '@/components/TextLink';
import SocialSignInList from '@/containers/SocialSignInList';
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
import TermsAndPrivacyLinks from '@/containers/TermsAndPrivacyLinks';
import { useSieMethods } from '@/hooks/use-sie';
import useTerms from '@/hooks/use-terms';

import ErrorPage from '../ErrorPage';

import IdentifierRegisterForm from './IdentifierRegisterForm';
import * as styles from './index.module.scss';

const RegisterFooter = () => {
const { t } = useTranslation();
const { signUpMethods, socialConnectors, signInMode, signInMethods, singleSignOnEnabled } =
useSieMethods();

const { t } = useTranslation();
const { termsValidation, agreeToTermsPolicy } = useTerms();
const navigate = useNavigate();

const { showSingleSignOnForm } = useContext(SingleSignOnFormModeContext);

const handleSsoNavigation = useCallback(async () => {
if (agreeToTermsPolicy === AgreeToTermsPolicy.Manual && !(await termsValidation())) {
return;
}

navigate('/single-sign-on/email');
}, [agreeToTermsPolicy, navigate, termsValidation]);

/* Hide footers when showing Single Sign On form */
if (showSingleSignOnForm) {
return null;
Expand All @@ -35,10 +46,22 @@ const RegisterFooter = () => {
{
// Single Sign On footer
singleSignOnEnabled && (
<div className={styles.singleSignOn}>
{t('description.use')}{' '}
<TextLink to="/single-sign-on/email" text="action.single_sign_on" />
</div>
<>
<div className={styles.singleSignOn}>
{t('description.use')}{' '}
<TextLink text="action.single_sign_on" onClick={handleSsoNavigation} />
</div>
{
/**
* If only SSO sign-in methods are available, display the agreement checkbox when the agreement policy is `Manual`.
*/
signInMethods.length === 0 &&
socialConnectors.length === 0 &&
agreeToTermsPolicy === AgreeToTermsPolicy.Manual && (
<TermsAndPrivacyCheckbox className={styles.checkbox} />
)
}
</>
)
}
{
Expand All @@ -64,6 +87,7 @@ const RegisterFooter = () => {

const Register = () => {
const { signUpMethods, socialConnectors, signInMode } = useSieMethods();
const { agreeToTermsPolicy } = useTerms();

if (!signInMode) {
return <ErrorPage />;
Expand All @@ -82,11 +106,16 @@ const Register = () => {
{/* Social sign-in methods only */}
{signUpMethods.length === 0 && socialConnectors.length > 0 && (
<>
<TermsAndPrivacyCheckbox className={styles.terms} />
{agreeToTermsPolicy !== AgreeToTermsPolicy.Automatic && (
<TermsAndPrivacyCheckbox className={styles.terms} />
)}
<SocialSignInList className={styles.main} socialConnectors={socialConnectors} />
</>
)}
<RegisterFooter />
{agreeToTermsPolicy === AgreeToTermsPolicy.Automatic && (
<TermsAndPrivacyLinks className={styles.terms} />
)}
</SingleSignOnFormModeContextProvider>

{/* Hide footer elements when showing Single Sign On form */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@
margin-left: _.unit(0.5);
margin-top: _.unit(-3);
}

.hidden {
display: none;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SignIn } from '@logto/schemas';
import { AgreeToTermsPolicy, type SignIn } from '@logto/schemas';
import classNames from 'classnames';
import { useCallback, useEffect, useMemo } from 'react';
import { useForm, Controller } from 'react-hook-form';
Expand All @@ -9,7 +9,9 @@ import Button from '@/components/Button';
import ErrorMessage from '@/components/ErrorMessage';
import { SmartInputField } from '@/components/InputFields';
import type { IdentifierInputValue } from '@/components/InputFields/SmartInputField';
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
import useSingleSignOnWatch from '@/hooks/use-single-sign-on-watch';
import useTerms from '@/hooks/use-terms';
import { getGeneralIdentifierErrorMessage, validateIdentifierField } from '@/utils/form';

import * as styles from './index.module.scss';
Expand All @@ -29,6 +31,7 @@ type FormState = {
const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) => {
const { t } = useTranslation();
const { errorMessage, clearErrorMessage, onSubmit } = useOnSubmit(signInMethods);
const { termsValidation, agreeToTermsPolicy } = useTerms();

const enabledSignInMethods = useMemo(
() => signInMethods.map(({ identifier }) => identifier),
Expand Down Expand Up @@ -69,10 +72,21 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
return;
}

if (!(await termsValidation())) {
return;
}

await onSubmit(type, value);
})(event);
},
[clearErrorMessage, handleSubmit, navigateToSingleSignOn, onSubmit, showSingleSignOnForm]
[
clearErrorMessage,
handleSubmit,
navigateToSingleSignOn,
onSubmit,
showSingleSignOnForm,
termsValidation,
]
);

return (
Expand Down Expand Up @@ -111,6 +125,21 @@ const IdentifierSignInForm = ({ className, autoFocus, signInMethods }: Props) =>
<div className={styles.message}>{t('description.single_sign_on_enabled')}</div>
)}

{/**
* Have to use css to hide the terms element.
* Remove element from dom will trigger a form re-render.
* Form rerender will trigger autofill.
* If the autofill value is SSO enabled, it will always show SSO form.
*/}
<TermsAndPrivacyCheckbox
className={classNames(
styles.terms,
// For sign in, only show the terms checkbox if the terms policy is manual
(showSingleSignOnForm || agreeToTermsPolicy !== AgreeToTermsPolicy.Manual) &&
styles.hidden
)}
/>

<Button
name="submit"
title={showSingleSignOnForm ? 'action.single_sign_on' : 'action.sign_in'}
Expand Down
21 changes: 19 additions & 2 deletions packages/experience/src/pages/SignIn/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { SignIn, ConnectorMetadata } from '@logto/schemas';
import { type SignIn, type ConnectorMetadata, AgreeToTermsPolicy } from '@logto/schemas';

import SocialSignInList from '@/containers/SocialSignInList';
import TermsAndPrivacyCheckbox from '@/containers/TermsAndPrivacyCheckbox';
import useTerms from '@/hooks/use-terms';

import IdentifierSignInForm from './IdentifierSignInForm';
import PasswordSignInForm from './PasswordSignInForm';
Expand All @@ -12,8 +14,23 @@ type Props = {
};

const Main = ({ signInMethods, socialConnectors }: Props) => {
const { agreeToTermsPolicy } = useTerms();

if (signInMethods.length === 0 && socialConnectors.length > 0) {
return <SocialSignInList className={styles.main} socialConnectors={socialConnectors} />;
return (
<>
<SocialSignInList className={styles.main} socialConnectors={socialConnectors} />
{
/**
* Display agreement checkbox when only social sign-in methods are available
* and the user needs to agree to terms manually.
*/
agreeToTermsPolicy === AgreeToTermsPolicy.Manual && (
<TermsAndPrivacyCheckbox className={styles.checkbox} />
)
}
</>
);
}

const isPasswordOnly =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
margin-left: _.unit(0.5);
margin-top: _.unit(-3);
}

.hidden {
display: none;
}
}

:global(.desktop) {
Expand Down
Loading

0 comments on commit 0595e3e

Please sign in to comment.