Skip to content

Commit

Permalink
fix(experience): allow link social account on sign-in only mode (#6560)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
simeng-li authored Sep 10, 2024
1 parent 8b19004 commit 2626616
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 104 deletions.
30 changes: 30 additions & 0 deletions .changeset/proud-books-itch.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SignInIdentifier } from '@logto/schemas';
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import classNames from 'classnames';
import type { TFuncKey } from 'i18next';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -41,7 +41,7 @@ const getCreateAccountActionText = (signUpMethods: string[]): TFuncKey => {

const SocialLinkAccount = ({ connectorId, className, relatedUser }: Props) => {
const { t } = useTranslation();
const { signUpMethods } = useSieMethods();
const { signUpMethods, signInMode } = useSieMethods();

const bindSocialRelatedUser = useBindSocialRelatedUser();
const registerWithSocial = useSocialRegister(connectorId);
Expand All @@ -65,17 +65,19 @@ const SocialLinkAccount = ({ connectorId, className, relatedUser }: Props) => {
}}
/>

<div className={styles.hint}>
<div>
<DynamicT forKey="description.skip_social_linking" />
{signInMode !== SignInMode.SignIn && (
<div className={styles.hint}>
<div>
<DynamicT forKey="description.skip_social_linking" />
</div>
<TextLink
text={actionText}
onClick={() => {
void registerWithSocial(connectorId);
}}
/>
</div>
<TextLink
text={actionText}
onClick={() => {
void registerWithSocial(connectorId);
}}
/>
</div>
)}
</div>
);
};
Expand Down
25 changes: 24 additions & 1 deletion packages/experience-legacy/src/hooks/use-social-register.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import { AgreeToTermsPolicy, experience } from '@logto/schemas';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';

import { registerWithVerifiedSocial } from '@/apis/interaction';

import useApi from './use-api';
import useErrorHandler from './use-error-handler';
import useGlobalRedirectTo from './use-global-redirect-to';
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
import useTerms from './use-terms';

const useSocialRegister = (connectorId?: string, replace?: boolean) => {
const handleError = useErrorHandler();
const asyncRegisterWithSocial = useApi(registerWithVerifiedSocial);
const redirectTo = useGlobalRedirectTo();
const { termsValidation, agreeToTermsPolicy } = useTerms();
const navigate = useNavigate();

const preSignInErrorHandler = usePreSignInErrorHandler({ linkSocial: connectorId, replace });

return useCallback(
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);

if (error) {
Expand All @@ -28,7 +43,15 @@ const useSocialRegister = (connectorId?: string, replace?: boolean) => {
await redirectTo(result.redirectTo);
}
},
[asyncRegisterWithSocial, handleError, preSignInErrorHandler, redirectTo]
[
agreeToTermsPolicy,
asyncRegisterWithSocial,
handleError,
navigate,
preSignInErrorHandler,
redirectTo,
termsValidation,
]
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GoogleConnector } from '@logto/connector-kit';
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 { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router-dom';
Expand All @@ -14,7 +14,6 @@ import useErrorHandler from '@/hooks/use-error-handler';
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
import { useSieMethods } from '@/hooks/use-sie';
import useSocialRegister from '@/hooks/use-social-register';
import useTerms from '@/hooks/use-terms';
import useToast from '@/hooks/use-toast';
import { socialAccountNotExistErrorDataGuard } from '@/types/guard';
import { parseQueryParameters } from '@/utils';
Expand All @@ -25,7 +24,6 @@ const useSocialSignInListener = (connectorId: string) => {
const { setToast } = useToast();
const { signInMode, socialSignInSettings } = useSieMethods();
const { t } = useTranslation();
const { termsValidation, agreeToTermsPolicy } = useTerms();
const [isConsumed, setIsConsumed] = useState(false);
const [searchParameters, setSearchParameters] = useSearchParams();

Expand All @@ -42,13 +40,16 @@ const useSocialSignInListener = (connectorId: string) => {
const { relatedUser } = data ?? {};

if (relatedUser) {
// If automatic account linking is enabled, bind the related user directly
if (socialSignInSettings.automaticAccountLinking) {
const { type, value } = relatedUser;

await bindSocialRelatedUser({
connectorId,
...(type === 'email' ? { email: value } : { phone: value }),
});
} else {
// Redirect to the social link page
navigate(`/social/link/${connectorId}`, {
replace: true,
state: { relatedUser },
Expand All @@ -58,6 +59,13 @@ const useSocialSignInListener = (connectorId: string) => {
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
await registerWithSocial(connectorId);
},
Expand All @@ -66,6 +74,8 @@ const useSocialSignInListener = (connectorId: string) => {
connectorId,
navigate,
registerWithSocial,
setToast,
signInMode,
socialSignInSettings.automaticAccountLinking,
]
);
Expand All @@ -74,42 +84,15 @@ const useSocialSignInListener = (connectorId: string) => {

const signInWithSocialErrorHandlers: ErrorHandlers = useMemo(
() => ({
'user.identity_not_exist': async (error) => {
// 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);
},
'user.identity_not_exist': accountNotExistErrorHandler,
...preSignInErrorHandler,
// Redirect to sign-in page if error is not handled by the error handlers
global: async (error) => {
setToast(error.message);
navigate('/' + experience.routes.signIn);
},
}),
[
preSignInErrorHandler,
signInMode,
agreeToTermsPolicy,
termsValidation,
accountNotExistErrorHandler,
setToast,
navigate,
]
[preSignInErrorHandler, accountNotExistErrorHandler, setToast, navigate]
);

const signInWithSocialHandler = useCallback(
Expand Down
26 changes: 14 additions & 12 deletions packages/experience/src/containers/SocialLinkAccount/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SignInIdentifier } from '@logto/schemas';
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import classNames from 'classnames';
import type { TFuncKey } from 'i18next';
import { useTranslation } from 'react-i18next';
Expand Down Expand Up @@ -42,7 +42,7 @@ const getCreateAccountActionText = (signUpMethods: string[]): TFuncKey => {

const SocialLinkAccount = ({ connectorId, verificationId, className, relatedUser }: Props) => {
const { t } = useTranslation();
const { signUpMethods } = useSieMethods();
const { signUpMethods, signInMode } = useSieMethods();

const bindSocialRelatedUser = useBindSocialRelatedUser();
const registerWithSocial = useSocialRegister(connectorId);
Expand All @@ -63,17 +63,19 @@ const SocialLinkAccount = ({ connectorId, verificationId, className, relatedUser
}}
/>

<div className={styles.hint}>
<div>
<DynamicT forKey="description.skip_social_linking" />
{signInMode !== SignInMode.SignIn && (
<div className={styles.hint}>
<div>
<DynamicT forKey="description.skip_social_linking" />
</div>
<TextLink
text={actionText}
onClick={() => {
void registerWithSocial(verificationId);
}}
/>
</div>
<TextLink
text={actionText}
onClick={() => {
void registerWithSocial(verificationId);
}}
/>
</div>
)}
</div>
);
};
Expand Down
26 changes: 24 additions & 2 deletions packages/experience/src/hooks/use-social-register.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { InteractionEvent } from '@logto/schemas';
import { AgreeToTermsPolicy, experience, InteractionEvent } from '@logto/schemas';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';

import { registerWithVerifiedIdentifier } from '@/apis/experience';

import useApi from './use-api';
import useErrorHandler from './use-error-handler';
import useGlobalRedirectTo from './use-global-redirect-to';
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
import useTerms from './use-terms';

const useSocialRegister = (connectorId: string, replace?: boolean) => {
const handleError = useErrorHandler();
const asyncRegisterWithSocial = useApi(registerWithVerifiedIdentifier);
const redirectTo = useGlobalRedirectTo();
const { termsValidation, agreeToTermsPolicy } = useTerms();
const navigate = useNavigate();

const preRegisterErrorHandler = usePreSignInErrorHandler({
linkSocial: connectorId,
Expand All @@ -21,6 +25,16 @@ const useSocialRegister = (connectorId: string, replace?: boolean) => {

return useCallback(
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);

if (error) {
Expand All @@ -33,7 +47,15 @@ const useSocialRegister = (connectorId: string, replace?: boolean) => {
await redirectTo(result.redirectTo);
}
},
[asyncRegisterWithSocial, handleError, preRegisterErrorHandler, redirectTo]
[
agreeToTermsPolicy,
asyncRegisterWithSocial,
handleError,
navigate,
preRegisterErrorHandler,
redirectTo,
termsValidation,
]
);
};

Expand Down
Loading

0 comments on commit 2626616

Please sign in to comment.