Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(experience): allow link social account on sign-in only mode #6560

Merged
merged 3 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading