mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
fix(experience): allow link social account on sign-in only mode (#6560)
* fix(experience): allow link social account on sign-in only mode allow link social account, when registration is disabled; * chore: add changeset add changeset * chore: fix typos fix typos
This commit is contained in:
parent
8b19004102
commit
2626616775
8 changed files with 165 additions and 104 deletions
30
.changeset/proud-books-itch.md
Normal file
30
.changeset/proud-books-itch.md
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
"@logto/experience-legacy": minor
|
||||||
|
"@logto/experience": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
allow link new social identity to an existing user account when registration is disabled.
|
||||||
|
|
||||||
|
### Previous behavior
|
||||||
|
|
||||||
|
Sign-in with a social identity that does not have an existing user account will throw an `identity_not_exist` error. When the registration is disabled, the error message will be shown, the user will not be able to create a new account or link the social identity to an existing account via verified email or phone number.
|
||||||
|
|
||||||
|
### Expected behavior
|
||||||
|
|
||||||
|
When the registration is disabled, if a related user account is found, the user should be able to link the social identity to an existing account via a verified email or phone number.
|
||||||
|
|
||||||
|
### Updates
|
||||||
|
|
||||||
|
When the registration is disabled:
|
||||||
|
|
||||||
|
- Show `identity_not_exist` error message if no related user account is found.
|
||||||
|
- Automatically link the social identity to the existing account if a related user account is found and social automatic account linking is enabled.
|
||||||
|
- Redirect the user to the social link account page if a related user account is found and social automatic account linking is disabled.
|
||||||
|
- Hide the register button on the social link account page if the registration is disabled.
|
||||||
|
|
||||||
|
When the registration is enabled:
|
||||||
|
|
||||||
|
- Automatically register a new account with the social identity if no related user account is found.
|
||||||
|
- Automatically link the social identity to the existing account if a related user account is found and social automatic account linking is enabled.
|
||||||
|
- Redirect the user to the social link account page if a related user account is found and social automatic account linking is disabled.
|
||||||
|
- Show the register new account button on the social link account page.
|
|
@ -1,4 +1,4 @@
|
||||||
import { SignInIdentifier } from '@logto/schemas';
|
import { SignInIdentifier, SignInMode } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { TFuncKey } from 'i18next';
|
import type { TFuncKey } from 'i18next';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -41,7 +41,7 @@ const getCreateAccountActionText = (signUpMethods: string[]): TFuncKey => {
|
||||||
|
|
||||||
const SocialLinkAccount = ({ connectorId, className, relatedUser }: Props) => {
|
const SocialLinkAccount = ({ connectorId, className, relatedUser }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { signUpMethods } = useSieMethods();
|
const { signUpMethods, signInMode } = useSieMethods();
|
||||||
|
|
||||||
const bindSocialRelatedUser = useBindSocialRelatedUser();
|
const bindSocialRelatedUser = useBindSocialRelatedUser();
|
||||||
const registerWithSocial = useSocialRegister(connectorId);
|
const registerWithSocial = useSocialRegister(connectorId);
|
||||||
|
@ -65,17 +65,19 @@ const SocialLinkAccount = ({ connectorId, className, relatedUser }: Props) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.hint}>
|
{signInMode !== SignInMode.SignIn && (
|
||||||
<div>
|
<div className={styles.hint}>
|
||||||
<DynamicT forKey="description.skip_social_linking" />
|
<div>
|
||||||
|
<DynamicT forKey="description.skip_social_linking" />
|
||||||
|
</div>
|
||||||
|
<TextLink
|
||||||
|
text={actionText}
|
||||||
|
onClick={() => {
|
||||||
|
void registerWithSocial(connectorId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TextLink
|
)}
|
||||||
text={actionText}
|
|
||||||
onClick={() => {
|
|
||||||
void registerWithSocial(connectorId);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { AgreeToTermsPolicy, experience } from '@logto/schemas';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { registerWithVerifiedSocial } from '@/apis/interaction';
|
import { registerWithVerifiedSocial } from '@/apis/interaction';
|
||||||
|
|
||||||
|
@ -6,16 +8,29 @@ import useApi from './use-api';
|
||||||
import useErrorHandler from './use-error-handler';
|
import useErrorHandler from './use-error-handler';
|
||||||
import useGlobalRedirectTo from './use-global-redirect-to';
|
import useGlobalRedirectTo from './use-global-redirect-to';
|
||||||
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
|
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
|
||||||
|
import useTerms from './use-terms';
|
||||||
|
|
||||||
const useSocialRegister = (connectorId?: string, replace?: boolean) => {
|
const useSocialRegister = (connectorId?: string, replace?: boolean) => {
|
||||||
const handleError = useErrorHandler();
|
const handleError = useErrorHandler();
|
||||||
const asyncRegisterWithSocial = useApi(registerWithVerifiedSocial);
|
const asyncRegisterWithSocial = useApi(registerWithVerifiedSocial);
|
||||||
const redirectTo = useGlobalRedirectTo();
|
const redirectTo = useGlobalRedirectTo();
|
||||||
|
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const preSignInErrorHandler = usePreSignInErrorHandler({ linkSocial: connectorId, replace });
|
const preSignInErrorHandler = usePreSignInErrorHandler({ linkSocial: connectorId, replace });
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async (connectorId: string) => {
|
async (connectorId: string) => {
|
||||||
|
/**
|
||||||
|
* Agree to terms and conditions first before proceeding
|
||||||
|
* If the agreement policy is `Manual`, the user must agree to the terms to reach this step.
|
||||||
|
* Therefore, skip the check for `Manual` policy.
|
||||||
|
*/
|
||||||
|
if (agreeToTermsPolicy !== AgreeToTermsPolicy.Manual && !(await termsValidation())) {
|
||||||
|
navigate('/' + experience.routes.signIn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [error, result] = await asyncRegisterWithSocial(connectorId);
|
const [error, result] = await asyncRegisterWithSocial(connectorId);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -28,7 +43,15 @@ const useSocialRegister = (connectorId?: string, replace?: boolean) => {
|
||||||
await redirectTo(result.redirectTo);
|
await redirectTo(result.redirectTo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[asyncRegisterWithSocial, handleError, preSignInErrorHandler, redirectTo]
|
[
|
||||||
|
agreeToTermsPolicy,
|
||||||
|
asyncRegisterWithSocial,
|
||||||
|
handleError,
|
||||||
|
navigate,
|
||||||
|
preSignInErrorHandler,
|
||||||
|
redirectTo,
|
||||||
|
termsValidation,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { GoogleConnector } from '@logto/connector-kit';
|
import { GoogleConnector } from '@logto/connector-kit';
|
||||||
import type { RequestErrorBody } from '@logto/schemas';
|
import type { RequestErrorBody } from '@logto/schemas';
|
||||||
import { AgreeToTermsPolicy, InteractionEvent, SignInMode, experience } from '@logto/schemas';
|
import { InteractionEvent, SignInMode, experience } from '@logto/schemas';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
@ -14,7 +14,6 @@ import useErrorHandler from '@/hooks/use-error-handler';
|
||||||
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
|
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
|
||||||
import { useSieMethods } from '@/hooks/use-sie';
|
import { useSieMethods } from '@/hooks/use-sie';
|
||||||
import useSocialRegister from '@/hooks/use-social-register';
|
import useSocialRegister from '@/hooks/use-social-register';
|
||||||
import useTerms from '@/hooks/use-terms';
|
|
||||||
import useToast from '@/hooks/use-toast';
|
import useToast from '@/hooks/use-toast';
|
||||||
import { socialAccountNotExistErrorDataGuard } from '@/types/guard';
|
import { socialAccountNotExistErrorDataGuard } from '@/types/guard';
|
||||||
import { parseQueryParameters } from '@/utils';
|
import { parseQueryParameters } from '@/utils';
|
||||||
|
@ -25,7 +24,6 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
const { setToast } = useToast();
|
const { setToast } = useToast();
|
||||||
const { signInMode, socialSignInSettings } = useSieMethods();
|
const { signInMode, socialSignInSettings } = useSieMethods();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
|
||||||
const [isConsumed, setIsConsumed] = useState(false);
|
const [isConsumed, setIsConsumed] = useState(false);
|
||||||
const [searchParameters, setSearchParameters] = useSearchParams();
|
const [searchParameters, setSearchParameters] = useSearchParams();
|
||||||
|
|
||||||
|
@ -42,13 +40,16 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
const { relatedUser } = data ?? {};
|
const { relatedUser } = data ?? {};
|
||||||
|
|
||||||
if (relatedUser) {
|
if (relatedUser) {
|
||||||
|
// If automatic account linking is enabled, bind the related user directly
|
||||||
if (socialSignInSettings.automaticAccountLinking) {
|
if (socialSignInSettings.automaticAccountLinking) {
|
||||||
const { type, value } = relatedUser;
|
const { type, value } = relatedUser;
|
||||||
|
|
||||||
await bindSocialRelatedUser({
|
await bindSocialRelatedUser({
|
||||||
connectorId,
|
connectorId,
|
||||||
...(type === 'email' ? { email: value } : { phone: value }),
|
...(type === 'email' ? { email: value } : { phone: value }),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
// Redirect to the social link page
|
||||||
navigate(`/social/link/${connectorId}`, {
|
navigate(`/social/link/${connectorId}`, {
|
||||||
replace: true,
|
replace: true,
|
||||||
state: { relatedUser },
|
state: { relatedUser },
|
||||||
|
@ -58,6 +59,13 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should not let user register new social account under sign-in only mode
|
||||||
|
if (signInMode === SignInMode.SignIn) {
|
||||||
|
setToast(error.message);
|
||||||
|
navigate('/' + experience.routes.signIn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Register with social
|
// Register with social
|
||||||
await registerWithSocial(connectorId);
|
await registerWithSocial(connectorId);
|
||||||
},
|
},
|
||||||
|
@ -66,6 +74,8 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
connectorId,
|
connectorId,
|
||||||
navigate,
|
navigate,
|
||||||
registerWithSocial,
|
registerWithSocial,
|
||||||
|
setToast,
|
||||||
|
signInMode,
|
||||||
socialSignInSettings.automaticAccountLinking,
|
socialSignInSettings.automaticAccountLinking,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -74,26 +84,7 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
|
|
||||||
const signInWithSocialErrorHandlers: ErrorHandlers = useMemo(
|
const signInWithSocialErrorHandlers: ErrorHandlers = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
'user.identity_not_exist': async (error) => {
|
'user.identity_not_exist': accountNotExistErrorHandler,
|
||||||
// Should not let user register new social account under sign-in only mode
|
|
||||||
if (signInMode === SignInMode.SignIn) {
|
|
||||||
setToast(error.message);
|
|
||||||
navigate('/' + experience.routes.signIn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agree to terms and conditions first before proceeding
|
|
||||||
* If the agreement policy is `Manual`, the user must agree to the terms to reach this step.
|
|
||||||
* Therefore, skip the check for `Manual` policy.
|
|
||||||
*/
|
|
||||||
if (agreeToTermsPolicy !== AgreeToTermsPolicy.Manual && !(await termsValidation())) {
|
|
||||||
navigate('/' + experience.routes.signIn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await accountNotExistErrorHandler(error);
|
|
||||||
},
|
|
||||||
...preSignInErrorHandler,
|
...preSignInErrorHandler,
|
||||||
// Redirect to sign-in page if error is not handled by the error handlers
|
// Redirect to sign-in page if error is not handled by the error handlers
|
||||||
global: async (error) => {
|
global: async (error) => {
|
||||||
|
@ -101,15 +92,7 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
navigate('/' + experience.routes.signIn);
|
navigate('/' + experience.routes.signIn);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[preSignInErrorHandler, accountNotExistErrorHandler, setToast, navigate]
|
||||||
preSignInErrorHandler,
|
|
||||||
signInMode,
|
|
||||||
agreeToTermsPolicy,
|
|
||||||
termsValidation,
|
|
||||||
accountNotExistErrorHandler,
|
|
||||||
setToast,
|
|
||||||
navigate,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const signInWithSocialHandler = useCallback(
|
const signInWithSocialHandler = useCallback(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SignInIdentifier } from '@logto/schemas';
|
import { SignInIdentifier, SignInMode } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { TFuncKey } from 'i18next';
|
import type { TFuncKey } from 'i18next';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -42,7 +42,7 @@ const getCreateAccountActionText = (signUpMethods: string[]): TFuncKey => {
|
||||||
|
|
||||||
const SocialLinkAccount = ({ connectorId, verificationId, className, relatedUser }: Props) => {
|
const SocialLinkAccount = ({ connectorId, verificationId, className, relatedUser }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { signUpMethods } = useSieMethods();
|
const { signUpMethods, signInMode } = useSieMethods();
|
||||||
|
|
||||||
const bindSocialRelatedUser = useBindSocialRelatedUser();
|
const bindSocialRelatedUser = useBindSocialRelatedUser();
|
||||||
const registerWithSocial = useSocialRegister(connectorId);
|
const registerWithSocial = useSocialRegister(connectorId);
|
||||||
|
@ -63,17 +63,19 @@ const SocialLinkAccount = ({ connectorId, verificationId, className, relatedUser
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.hint}>
|
{signInMode !== SignInMode.SignIn && (
|
||||||
<div>
|
<div className={styles.hint}>
|
||||||
<DynamicT forKey="description.skip_social_linking" />
|
<div>
|
||||||
|
<DynamicT forKey="description.skip_social_linking" />
|
||||||
|
</div>
|
||||||
|
<TextLink
|
||||||
|
text={actionText}
|
||||||
|
onClick={() => {
|
||||||
|
void registerWithSocial(verificationId);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TextLink
|
)}
|
||||||
text={actionText}
|
|
||||||
onClick={() => {
|
|
||||||
void registerWithSocial(verificationId);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { InteractionEvent } from '@logto/schemas';
|
import { AgreeToTermsPolicy, experience, InteractionEvent } from '@logto/schemas';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { registerWithVerifiedIdentifier } from '@/apis/experience';
|
import { registerWithVerifiedIdentifier } from '@/apis/experience';
|
||||||
|
|
||||||
|
@ -7,11 +8,14 @@ import useApi from './use-api';
|
||||||
import useErrorHandler from './use-error-handler';
|
import useErrorHandler from './use-error-handler';
|
||||||
import useGlobalRedirectTo from './use-global-redirect-to';
|
import useGlobalRedirectTo from './use-global-redirect-to';
|
||||||
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
|
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
|
||||||
|
import useTerms from './use-terms';
|
||||||
|
|
||||||
const useSocialRegister = (connectorId: string, replace?: boolean) => {
|
const useSocialRegister = (connectorId: string, replace?: boolean) => {
|
||||||
const handleError = useErrorHandler();
|
const handleError = useErrorHandler();
|
||||||
const asyncRegisterWithSocial = useApi(registerWithVerifiedIdentifier);
|
const asyncRegisterWithSocial = useApi(registerWithVerifiedIdentifier);
|
||||||
const redirectTo = useGlobalRedirectTo();
|
const redirectTo = useGlobalRedirectTo();
|
||||||
|
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const preRegisterErrorHandler = usePreSignInErrorHandler({
|
const preRegisterErrorHandler = usePreSignInErrorHandler({
|
||||||
linkSocial: connectorId,
|
linkSocial: connectorId,
|
||||||
|
@ -21,6 +25,16 @@ const useSocialRegister = (connectorId: string, replace?: boolean) => {
|
||||||
|
|
||||||
return useCallback(
|
return useCallback(
|
||||||
async (verificationId: string) => {
|
async (verificationId: string) => {
|
||||||
|
/**
|
||||||
|
* Agree to terms and conditions first before proceeding
|
||||||
|
* If the agreement policy is `Manual`, the user must agree to the terms to reach this step.
|
||||||
|
* Therefore, skip the check for `Manual` policy.
|
||||||
|
*/
|
||||||
|
if (agreeToTermsPolicy !== AgreeToTermsPolicy.Manual && !(await termsValidation())) {
|
||||||
|
navigate('/' + experience.routes.signIn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const [error, result] = await asyncRegisterWithSocial(verificationId);
|
const [error, result] = await asyncRegisterWithSocial(verificationId);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -33,7 +47,15 @@ const useSocialRegister = (connectorId: string, replace?: boolean) => {
|
||||||
await redirectTo(result.redirectTo);
|
await redirectTo(result.redirectTo);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[asyncRegisterWithSocial, handleError, preRegisterErrorHandler, redirectTo]
|
[
|
||||||
|
agreeToTermsPolicy,
|
||||||
|
asyncRegisterWithSocial,
|
||||||
|
handleError,
|
||||||
|
navigate,
|
||||||
|
preRegisterErrorHandler,
|
||||||
|
redirectTo,
|
||||||
|
termsValidation,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import { GoogleConnector } from '@logto/connector-kit';
|
import { GoogleConnector } from '@logto/connector-kit';
|
||||||
import type { RequestErrorBody } from '@logto/schemas';
|
import type { RequestErrorBody } from '@logto/schemas';
|
||||||
import {
|
import { InteractionEvent, SignInMode, VerificationType, experience } from '@logto/schemas';
|
||||||
AgreeToTermsPolicy,
|
|
||||||
InteractionEvent,
|
|
||||||
SignInMode,
|
|
||||||
VerificationType,
|
|
||||||
experience,
|
|
||||||
} from '@logto/schemas';
|
|
||||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
@ -25,7 +19,6 @@ import useErrorHandler from '@/hooks/use-error-handler';
|
||||||
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
|
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
|
||||||
import { useSieMethods } from '@/hooks/use-sie';
|
import { useSieMethods } from '@/hooks/use-sie';
|
||||||
import useSocialRegister from '@/hooks/use-social-register';
|
import useSocialRegister from '@/hooks/use-social-register';
|
||||||
import useTerms from '@/hooks/use-terms';
|
|
||||||
import useToast from '@/hooks/use-toast';
|
import useToast from '@/hooks/use-toast';
|
||||||
import { socialAccountNotExistErrorDataGuard } from '@/types/guard';
|
import { socialAccountNotExistErrorDataGuard } from '@/types/guard';
|
||||||
import { parseQueryParameters } from '@/utils';
|
import { parseQueryParameters } from '@/utils';
|
||||||
|
@ -36,7 +29,6 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
const { setToast } = useToast();
|
const { setToast } = useToast();
|
||||||
const { signInMode, socialSignInSettings } = useSieMethods();
|
const { signInMode, socialSignInSettings } = useSieMethods();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { termsValidation, agreeToTermsPolicy } = useTerms();
|
|
||||||
const [isConsumed, setIsConsumed] = useState(false);
|
const [isConsumed, setIsConsumed] = useState(false);
|
||||||
const [searchParameters, setSearchParameters] = useSearchParams();
|
const [searchParameters, setSearchParameters] = useSearchParams();
|
||||||
const { verificationIdsMap, setVerificationId } = useContext(UserInteractionContext);
|
const { verificationIdsMap, setVerificationId } = useContext(UserInteractionContext);
|
||||||
|
@ -80,6 +72,13 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should not let user register new social account under sign-in only mode
|
||||||
|
if (signInMode === SignInMode.SignIn) {
|
||||||
|
setToast(error.message);
|
||||||
|
navigate('/' + experience.routes.signIn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Register with social
|
// Register with social
|
||||||
await registerWithSocial(verificationId);
|
await registerWithSocial(verificationId);
|
||||||
},
|
},
|
||||||
|
@ -89,6 +88,7 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
navigate,
|
navigate,
|
||||||
registerWithSocial,
|
registerWithSocial,
|
||||||
setToast,
|
setToast,
|
||||||
|
signInMode,
|
||||||
socialSignInSettings.automaticAccountLinking,
|
socialSignInSettings.automaticAccountLinking,
|
||||||
t,
|
t,
|
||||||
]
|
]
|
||||||
|
@ -106,39 +106,11 @@ const useSocialSignInListener = (connectorId: string) => {
|
||||||
|
|
||||||
const signInWithSocialErrorHandlers: ErrorHandlers = useMemo(
|
const signInWithSocialErrorHandlers: ErrorHandlers = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
'user.identity_not_exist': async (error) => {
|
'user.identity_not_exist': accountNotExistErrorHandler,
|
||||||
// Should not let user register new social account under sign-in only mode
|
|
||||||
if (signInMode === SignInMode.SignIn) {
|
|
||||||
setToast(error.message);
|
|
||||||
navigate('/' + experience.routes.signIn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agree to terms and conditions first before proceeding
|
|
||||||
* If the agreement policy is `Manual`, the user must agree to the terms to reach this step.
|
|
||||||
* Therefore, skip the check for `Manual` policy.
|
|
||||||
*/
|
|
||||||
if (agreeToTermsPolicy !== AgreeToTermsPolicy.Manual && !(await termsValidation())) {
|
|
||||||
navigate('/' + experience.routes.signIn);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await accountNotExistErrorHandler(error);
|
|
||||||
},
|
|
||||||
...preSignInErrorHandler,
|
...preSignInErrorHandler,
|
||||||
global: globalErrorHandler,
|
global: globalErrorHandler,
|
||||||
}),
|
}),
|
||||||
[
|
[preSignInErrorHandler, globalErrorHandler, accountNotExistErrorHandler]
|
||||||
preSignInErrorHandler,
|
|
||||||
globalErrorHandler,
|
|
||||||
signInMode,
|
|
||||||
agreeToTermsPolicy,
|
|
||||||
termsValidation,
|
|
||||||
accountNotExistErrorHandler,
|
|
||||||
setToast,
|
|
||||||
navigate,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const verifySocialCallbackData = useCallback(
|
const verifySocialCallbackData = useCallback(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ConnectorType } from '@logto/connector-kit';
|
import { ConnectorType } from '@logto/connector-kit';
|
||||||
import { AgreeToTermsPolicy, SignInIdentifier } from '@logto/schemas';
|
import { AgreeToTermsPolicy, SignInIdentifier, SignInMode } from '@logto/schemas';
|
||||||
|
|
||||||
import { createUser, deleteUser } from '#src/api/admin-user.js';
|
import { createUser, deleteUser } from '#src/api/admin-user.js';
|
||||||
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
import { updateSignInExperience } from '#src/api/sign-in-experience.js';
|
||||||
|
@ -55,7 +55,38 @@ describe('automatic account linking', () => {
|
||||||
await deleteUser(user.id);
|
await deleteUser(user.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should automatically link account with terms of use and privacy policy', async () => {
|
it('should automatically link account even if the registration is disabled', async () => {
|
||||||
|
await updateSignInExperience({
|
||||||
|
termsOfUseUrl: null,
|
||||||
|
privacyPolicyUrl: null,
|
||||||
|
socialSignIn: { automaticAccountLinking: true },
|
||||||
|
signInMode: SignInMode.SignIn,
|
||||||
|
});
|
||||||
|
|
||||||
|
const socialUserId = 'foo_' + randomString();
|
||||||
|
const user = await createUser({ primaryEmail: generateEmail() });
|
||||||
|
const experience = new ExpectExperience(await browser.newPage());
|
||||||
|
|
||||||
|
await experience.navigateTo(demoAppUrl.href);
|
||||||
|
await experience.toProcessSocialSignIn({
|
||||||
|
socialUserId,
|
||||||
|
socialEmail: user.primaryEmail!,
|
||||||
|
});
|
||||||
|
|
||||||
|
experience.toMatchUrl(demoAppUrl);
|
||||||
|
await experience.toMatchElement('div', { text: `User ID: ${user.id}` });
|
||||||
|
await experience.toClick('div[role=button]', /sign out/i);
|
||||||
|
await experience.page.close();
|
||||||
|
|
||||||
|
await deleteUser(user.id);
|
||||||
|
|
||||||
|
// Reset the sign-in experience
|
||||||
|
await updateSignInExperience({
|
||||||
|
signInMode: SignInMode.SignInAndRegister,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should automatically link account without verify terms of use', async () => {
|
||||||
await updateSignInExperience({
|
await updateSignInExperience({
|
||||||
termsOfUseUrl: 'https://example.com/terms',
|
termsOfUseUrl: 'https://example.com/terms',
|
||||||
privacyPolicyUrl: 'https://example.com/privacy',
|
privacyPolicyUrl: 'https://example.com/privacy',
|
||||||
|
@ -72,10 +103,6 @@ describe('automatic account linking', () => {
|
||||||
socialEmail: user.primaryEmail!,
|
socialEmail: user.primaryEmail!,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Should have popped up the terms of use and privacy policy dialog
|
|
||||||
await experience.toMatchElement('div', { text: /terms of use/i });
|
|
||||||
await experience.toClick('button', /agree/i);
|
|
||||||
|
|
||||||
experience.toMatchUrl(demoAppUrl);
|
experience.toMatchUrl(demoAppUrl);
|
||||||
await experience.toMatchElement('div', { text: `User ID: ${user.id}` });
|
await experience.toMatchElement('div', { text: `User ID: ${user.id}` });
|
||||||
await experience.toClick('div[role=button]', /sign out/i);
|
await experience.toClick('div[role=button]', /sign out/i);
|
||||||
|
|
Loading…
Reference in a new issue