mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
feat(core, console, experience): remove dev feature guards (#7211)
* feat(core, console, experience): remove dev feature guards remove dev feature guards * fix(console): remove console log remove console log * chore: update changeset update changeset * chore: update changeset content update changeset content * fix(experience): should nav back to email form should nav back to email form is user cancel the direct sign-in modal.
This commit is contained in:
parent
e502607ddf
commit
13d04d7766
24 changed files with 334 additions and 1457 deletions
100
.changeset/hot-dodos-refuse.md
Normal file
100
.changeset/hot-dodos-refuse.md
Normal file
|
@ -0,0 +1,100 @@
|
|||
---
|
||||
"@logto/schemas": minor
|
||||
"@logto/core": minor
|
||||
"@logto/console": minor
|
||||
"@logto/experience": minor
|
||||
---
|
||||
|
||||
feat: support multiple sign-up identifiers in sign-in experience
|
||||
|
||||
## New update
|
||||
|
||||
Introduces a new optional field, `secondaryIdentifiers`, to the sign-in experience sign-up settings. This enhancement allows developers to specify multiple required user identifiers during the user sign-up process. Available options include `email`, `phone`, `username` and `emailOrPhone`.
|
||||
|
||||
### Explanation of the difference between `signUp.identifiers` and new `signUp.secondaryIdentifiers`
|
||||
|
||||
The existing `signUp.identifiers` field represents the sign-up identifiers enabled for user sign-up and is an array type. In this legacy setup, if multiple identifiers are provided, users can complete the sign-up process using any one of them. The only multi-value case allowed is `[email, phone]`, which signifies that users can provide either an email or a phone number.
|
||||
|
||||
To enhance flexibility and support multiple required sign-up identifiers, the existing `signUp.identifiers` field does not suffice. To maintain backward compatibility with existing data, we have introduced this new `secondaryIdentifiers` field.
|
||||
|
||||
Unlike the `signUp.identifiers` field, the `signUp.secondaryIdentifiers` array follows an `AND` logic, meaning that all elements listed in this field are required during the sign-up process, in addition to the primary identifiers. This new field also accommodates the `emailOrPhone` case by defining an exclusive `emailOrPhone` value type, which indicates that either a phone number or an email address must be provided.
|
||||
|
||||
In summary, while `identifiers` allows for optional selection among email and phone, `secondaryIdentifiers` enforces mandatory inclusion of all specified identifiers.
|
||||
|
||||
### Examples
|
||||
|
||||
1. `username` as the primary identifier. In addition, user will be required to provide a verified `email` and `phone number` during the sign-up process.
|
||||
|
||||
```json
|
||||
{
|
||||
"identifiers": ["username"],
|
||||
"secondaryIdentifiers": [
|
||||
{
|
||||
"type": "email",
|
||||
"verify": true
|
||||
},
|
||||
{
|
||||
"type": "phone",
|
||||
"verify": true
|
||||
}
|
||||
],
|
||||
"verify": true,
|
||||
"password": true
|
||||
}
|
||||
```
|
||||
|
||||
2. `username` as the primary identifier. In addition, user will be required to provide either a verified `email` or `phone number` during the sign-up process.
|
||||
|
||||
```json
|
||||
{
|
||||
"identifiers": ["username"],
|
||||
"secondaryIdentifiers": [
|
||||
{
|
||||
"type": "emailOrPhone",
|
||||
"verify": true
|
||||
}
|
||||
],
|
||||
"verify": true,
|
||||
"password": true
|
||||
}
|
||||
```
|
||||
|
||||
3. `email` or `phone number` as the primary identifier. In addition, user will be required to provide a `username` during the sign-up process.
|
||||
|
||||
```json
|
||||
{
|
||||
"identifiers": ["email", "phone"],
|
||||
"secondaryIdentifiers": [
|
||||
{
|
||||
"type": "username",
|
||||
"verify": true
|
||||
}
|
||||
],
|
||||
"verify": true,
|
||||
"password": false
|
||||
}
|
||||
```
|
||||
|
||||
### Sign-in experience settings
|
||||
|
||||
- `@logto/core`: Update the `/api/sign-in-experience` endpoint to support the new `secondaryIdentifiers` field in the sign-up settings.
|
||||
- `@logto/console`: Replace the sign-up identifier single selector with a multi-selector to support multiple sign-up identifiers. The order of the identifiers can be rearranged by dragging and dropping the items in the list. The first item in the list will be considered the primary identifier and stored in the `signUp.identifiers` field, while the rest will be stored in the `signUp.secondaryIdentifiers` field.
|
||||
|
||||
### End-user experience
|
||||
|
||||
The sign-up flow is now split into two stages:
|
||||
|
||||
- Primary identifiers (`signUp.identifiers`) are collected in the first-screen registration screen.
|
||||
- Secondary identifiers (`signUp.secondaryIdentifiers`) are requested in subsequent steps after the primary registration has been submitted.
|
||||
|
||||
## Other refactors
|
||||
|
||||
We have fully decoupled the sign-up identifier settings from the sign-in methods. Developers can now require as many user identifiers as needed during the sign-up process without impacting the sign-in process.
|
||||
|
||||
The following restrictions on sign-in and sign-up settings have been removed:
|
||||
|
||||
1. Password requirement is now optional when `username` is configured as a sign-up identifier. However, users without passwords cannot sign in using username authentication.
|
||||
|
||||
2. Removed the constraint requiring sign-up identifiers to be enabled as sign-in methods.
|
||||
|
||||
3. Removed the requirement for password verification across all sign-in methods when password is enabled for sign-up.
|
|
@ -135,7 +135,7 @@ function MfaForm({ data, onMfaUpdated }: Props) {
|
|||
// Only show the organization MFA policy config for the admin tenant
|
||||
const showOrganizationMfaPolicyConfig = useMemo(
|
||||
() => isDevFeaturesEnabled || (isCloud && currentTenantId === adminTenantId),
|
||||
[]
|
||||
[currentTenantId]
|
||||
);
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
|
|
|
@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next';
|
|||
import Draggable from '@/assets/icons/draggable.svg?react';
|
||||
import Minus from '@/assets/icons/minus.svg?react';
|
||||
import SwitchArrowIcon from '@/assets/icons/switch-arrow.svg?react';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import Checkbox from '@/ds-components/Checkbox';
|
||||
import IconButton from '@/ds-components/IconButton';
|
||||
import { Tooltip } from '@/ds-components/Tip';
|
||||
|
@ -66,12 +65,6 @@ function SignInMethodItem({
|
|||
label={t('sign_in_exp.sign_up_and_sign_in.sign_in.password_auth')}
|
||||
checked={password}
|
||||
disabled={!isPasswordCheckable}
|
||||
tooltip={conditional(
|
||||
// TODO:remove the password tool tip
|
||||
!isDevFeaturesEnabled &&
|
||||
!isPasswordCheckable &&
|
||||
t('sign_in_exp.sign_up_and_sign_in.tip.password_auth')
|
||||
)}
|
||||
onChange={(checked) => {
|
||||
onVerificationStateChange('password', checked);
|
||||
}}
|
||||
|
|
|
@ -4,18 +4,13 @@ import { useCallback } from 'react';
|
|||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { DragDropProvider, DraggableItem } from '@/ds-components/DragDrop';
|
||||
import useEnabledConnectorTypes from '@/hooks/use-enabled-connector-types';
|
||||
|
||||
import type { SignInExperienceForm } from '../../../../types';
|
||||
import { signInIdentifiers, signUpIdentifiersMapping } from '../../../constants';
|
||||
import { signInIdentifiers } from '../../../constants';
|
||||
import { identifierRequiredConnectorMapping } from '../../constants';
|
||||
import {
|
||||
getSignUpRequiredConnectorTypes,
|
||||
createSignInMethod,
|
||||
getSignUpIdentifiersRequiredConnectors,
|
||||
} from '../../utils';
|
||||
import { createSignInMethod, getSignUpIdentifiersRequiredConnectors } from '../../utils';
|
||||
|
||||
import AddButton from './AddButton';
|
||||
import SignInMethodItem from './SignInMethodItem';
|
||||
|
@ -47,20 +42,11 @@ function SignInMethodEditBox() {
|
|||
|
||||
const { isConnectorTypeEnabled } = useEnabledConnectorTypes();
|
||||
|
||||
const {
|
||||
identifier: signUpIdentifier,
|
||||
identifiers,
|
||||
password: isSignUpPasswordRequired,
|
||||
verify: isSignUpVerificationRequired,
|
||||
} = signUp;
|
||||
const { identifiers, password: isSignUpPasswordRequired } = signUp;
|
||||
|
||||
const requiredSignInIdentifiers = signUpIdentifiersMapping[signUpIdentifier];
|
||||
const signUpIdentifiers = identifiers.map(({ identifier }) => identifier);
|
||||
|
||||
// TODO: Remove this dev feature guard when multi sign-up identifiers are launched
|
||||
const ignoredWarningConnectors = isDevFeaturesEnabled
|
||||
? getSignUpIdentifiersRequiredConnectors(signUpIdentifiers)
|
||||
: getSignUpRequiredConnectorTypes(signUpIdentifier);
|
||||
const ignoredWarningConnectors = getSignUpIdentifiersRequiredConnectors(signUpIdentifiers);
|
||||
|
||||
const signInIdentifierOptions = signInIdentifiers.filter((candidateIdentifier) =>
|
||||
fields.every(({ identifier }) => identifier !== candidateIdentifier)
|
||||
|
@ -76,10 +62,6 @@ function SignInMethodEditBox() {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!isDevFeaturesEnabled) {
|
||||
return !isSignUpVerificationRequired;
|
||||
}
|
||||
|
||||
// If the email or phone sign-in method is enabled as one of the sign-up identifiers
|
||||
// and password is not required for sign-up, then verification code is required and uncheckable.
|
||||
// This is to ensure new users can sign in without password.
|
||||
|
@ -91,7 +73,7 @@ function SignInMethodEditBox() {
|
|||
|
||||
return !signUpVerificationRequired;
|
||||
},
|
||||
[isSignUpPasswordRequired, isSignUpVerificationRequired, signUpIdentifiers]
|
||||
[isSignUpPasswordRequired, signUpIdentifiers]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -141,15 +123,10 @@ function SignInMethodEditBox() {
|
|||
}}
|
||||
render={({ field: { value }, fieldState: { error } }) => (
|
||||
<SignInMethodItem
|
||||
isDeletable
|
||||
signInMethod={value}
|
||||
isPasswordCheckable={
|
||||
identifier !== SignInIdentifier.Username &&
|
||||
(isDevFeaturesEnabled || !isSignUpPasswordRequired)
|
||||
}
|
||||
isPasswordCheckable={identifier !== SignInIdentifier.Username}
|
||||
isVerificationCodeCheckable={isVerificationCodeCheckable(value.identifier)}
|
||||
isDeletable={
|
||||
isDevFeaturesEnabled || !requiredSignInIdentifiers.includes(identifier)
|
||||
}
|
||||
requiredConnectors={requiredConnectors}
|
||||
hasError={Boolean(error)}
|
||||
errorMessage={error?.message}
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Card from '@/ds-components/Card';
|
||||
import Checkbox from '@/ds-components/Checkbox';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
|
||||
import type { SignInExperienceForm } from '../../../types';
|
||||
import FormFieldDescription from '../../components/FormFieldDescription';
|
||||
import FormSectionTitle from '../../components/FormSectionTitle';
|
||||
|
||||
import SignUpIdentifiersEditBox from './SignUpIdentifiersEditBox';
|
||||
import styles from './index.module.scss';
|
||||
import useSignUpPasswordListeners from './use-sign-up-password-listeners';
|
||||
|
||||
function SignUpForm() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const { control } = useFormContext<SignInExperienceForm>();
|
||||
|
||||
const signUpIdentifiers = useWatch({
|
||||
control,
|
||||
name: 'signUp.identifiers',
|
||||
});
|
||||
|
||||
const signUpVerify = useWatch({
|
||||
control,
|
||||
name: 'signUp.verify',
|
||||
});
|
||||
|
||||
const showAuthenticationFields = useMemo(
|
||||
() => signUpIdentifiers.length > 0,
|
||||
[signUpIdentifiers.length]
|
||||
);
|
||||
|
||||
useSignUpPasswordListeners();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<FormSectionTitle title="sign_up_and_sign_in.sign_up.title" />
|
||||
<FormField title="sign_in_exp.sign_up_and_sign_in.sign_up.sign_up_identifier">
|
||||
<FormFieldDescription>
|
||||
{t('sign_in_exp.sign_up_and_sign_in.sign_up.identifier_description')}
|
||||
</FormFieldDescription>
|
||||
<SignUpIdentifiersEditBox />
|
||||
</FormField>
|
||||
{showAuthenticationFields && (
|
||||
<FormField title="sign_in_exp.sign_up_and_sign_in.sign_up.sign_up_authentication">
|
||||
<FormFieldDescription>
|
||||
{t('sign_in_exp.sign_up_and_sign_in.sign_up.authentication_description')}
|
||||
</FormFieldDescription>
|
||||
<div className={styles.selections}>
|
||||
<Controller
|
||||
name="signUp.password"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_up.set_a_password_option')}
|
||||
checked={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{signUpVerify && (
|
||||
<Controller
|
||||
name="signUp.verify"
|
||||
control={control}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
disabled
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_up.verify_at_sign_up_option')}
|
||||
checked={value}
|
||||
suffixTooltip={t('sign_in_exp.sign_up_and_sign_in.sign_up.verification_tip')}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FormField>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignUpForm;
|
|
@ -47,6 +47,18 @@ function SignUpIdentifiersEditBox() {
|
|||
name: 'signUp.identifiers',
|
||||
});
|
||||
|
||||
// Revalidate the sign-up and sign-in methods,
|
||||
// when appending or removing sign-up identifiers.
|
||||
const revalidateForm = useCallback(() => {
|
||||
if (submitCount > 0) {
|
||||
// Wait for the form re-render before validating the new data.
|
||||
setTimeout(() => {
|
||||
void trigger('signUp.identifiers');
|
||||
void trigger('signIn.methods');
|
||||
}, 0);
|
||||
}
|
||||
}, [submitCount, trigger]);
|
||||
|
||||
/**
|
||||
* Append the sign-in methods based on the selected sign-up identifier.
|
||||
*/
|
||||
|
@ -75,15 +87,8 @@ function SignUpIdentifiersEditBox() {
|
|||
shouldDirty: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (submitCount) {
|
||||
// Wait for the form re-render before validating the new data.
|
||||
setTimeout(() => {
|
||||
void trigger('signIn.methods');
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
[getValues, setValue, submitCount, trigger]
|
||||
[getValues, setValue]
|
||||
);
|
||||
|
||||
const onAppendSignUpIdentifier = useCallback(
|
||||
|
@ -195,6 +200,7 @@ function SignUpIdentifiersEditBox() {
|
|||
errorMessage={error?.message}
|
||||
onDelete={() => {
|
||||
remove(index);
|
||||
revalidateForm();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -209,6 +215,7 @@ function SignUpIdentifiersEditBox() {
|
|||
hasSelectedIdentifiers={signUpIdentifiers.length > 0}
|
||||
onSelected={(identifier) => {
|
||||
append({ identifier });
|
||||
revalidateForm();
|
||||
onAppendSignUpIdentifier(identifier);
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -1,119 +1,40 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useCallback } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import Card from '@/ds-components/Card';
|
||||
import Checkbox from '@/ds-components/Checkbox';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import Select from '@/ds-components/Select';
|
||||
import useEnabledConnectorTypes from '@/hooks/use-enabled-connector-types';
|
||||
|
||||
import { SignUpIdentifier } from '../../../types';
|
||||
import type { SignInExperienceForm } from '../../../types';
|
||||
import FormFieldDescription from '../../components/FormFieldDescription';
|
||||
import FormSectionTitle from '../../components/FormSectionTitle';
|
||||
import {
|
||||
signUpIdentifierPhrase,
|
||||
signUpIdentifiers,
|
||||
signUpIdentifiersMapping,
|
||||
} from '../../constants';
|
||||
import ConnectorSetupWarning from '../components/ConnectorSetupWarning';
|
||||
import {
|
||||
createSignInMethod,
|
||||
getSignUpRequiredConnectorTypes,
|
||||
isVerificationRequiredSignUpIdentifiers,
|
||||
} from '../utils';
|
||||
|
||||
import SignUpIdentifiersEditBox from './SignUpIdentifiersEditBox';
|
||||
import styles from './index.module.scss';
|
||||
import useSignUpPasswordListeners from './use-sign-up-password-listeners';
|
||||
|
||||
function SignUpForm() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
|
||||
const { control } = useFormContext<SignInExperienceForm>();
|
||||
|
||||
const signUpIdentifiers = useWatch({
|
||||
control,
|
||||
setValue,
|
||||
getValues,
|
||||
watch,
|
||||
trigger,
|
||||
formState: { errors, submitCount },
|
||||
} = useFormContext<SignInExperienceForm>();
|
||||
name: 'signUp.identifiers',
|
||||
});
|
||||
|
||||
const { isConnectorTypeEnabled } = useEnabledConnectorTypes();
|
||||
const signUpVerify = useWatch({
|
||||
control,
|
||||
name: 'signUp.verify',
|
||||
});
|
||||
|
||||
const signUp = watch('signUp');
|
||||
const { identifier: signUpIdentifier } = signUp;
|
||||
const isUsernamePasswordSignUp = signUpIdentifier === SignUpIdentifier.Username;
|
||||
|
||||
const postSignUpIdentifierChange = useCallback(
|
||||
(signUpIdentifier: SignUpIdentifier) => {
|
||||
if (signUpIdentifier === SignUpIdentifier.Username) {
|
||||
setValue('signUp.password', true);
|
||||
setValue('signUp.verify', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (signUpIdentifier === SignUpIdentifier.None) {
|
||||
setValue('signUp.password', false);
|
||||
setValue('signUp.verify', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVerificationRequiredSignUpIdentifiers(signUpIdentifier)) {
|
||||
setValue('signUp.verify', true);
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
const showAuthenticationFields = useMemo(
|
||||
() => signUpIdentifiers.length > 0,
|
||||
[signUpIdentifiers.length]
|
||||
);
|
||||
|
||||
const refreshSignInMethods = () => {
|
||||
const signInMethods = getValues('signIn.methods');
|
||||
const { verify, password, identifier } = signUp;
|
||||
const enabledSignUpIdentifiers = signUpIdentifiersMapping[identifier];
|
||||
|
||||
// Auto append newly assigned sign-up identifiers to the sign-in methods list if they don't already exist
|
||||
// User may remove them manually if they don't want to use it for sign-in.
|
||||
const mergedSignInMethods = enabledSignUpIdentifiers.reduce((methods, requiredIdentifier) => {
|
||||
if (signInMethods.some(({ identifier }) => identifier === requiredIdentifier)) {
|
||||
return methods;
|
||||
}
|
||||
|
||||
return [...methods, createSignInMethod(requiredIdentifier)];
|
||||
}, signInMethods);
|
||||
|
||||
// Note: if verification is required, but password is not set for sign-up, then
|
||||
// make sure all the email and phone sign-in methods have verification code enabled.
|
||||
const isVerificationCodeRequired = verify && !password;
|
||||
|
||||
setValue(
|
||||
'signIn.methods',
|
||||
// Note: refresh sign-in authentications according to the sign-up authentications config
|
||||
mergedSignInMethods.map((method) => {
|
||||
const { identifier } = method;
|
||||
|
||||
if (identifier === SignInIdentifier.Username) {
|
||||
return method;
|
||||
}
|
||||
|
||||
return {
|
||||
...method,
|
||||
// Auto enabled password for email and phone sign-in methods if password is required for sign-up.
|
||||
// User may disable it manually if they don't want to use password for email or phone sign-in.
|
||||
password: method.password || password,
|
||||
verificationCode: isVerificationCodeRequired ? true : method.verificationCode,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Note: we need to revalidate the sign-in methods after we have submitted
|
||||
if (submitCount) {
|
||||
// Note: wait for the form to be updated before validating the new data.
|
||||
setTimeout(() => {
|
||||
void trigger('signIn.methods');
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
useSignUpPasswordListeners();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
@ -122,51 +43,9 @@ function SignUpForm() {
|
|||
<FormFieldDescription>
|
||||
{t('sign_in_exp.sign_up_and_sign_in.sign_up.identifier_description')}
|
||||
</FormFieldDescription>
|
||||
<Controller
|
||||
name="signUp.identifier"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
return getSignUpRequiredConnectorTypes(value).every((connectorType) =>
|
||||
isConnectorTypeEnabled(connectorType)
|
||||
);
|
||||
},
|
||||
}}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select
|
||||
value={value}
|
||||
error={Boolean(errors.signUp?.identifier)}
|
||||
options={signUpIdentifiers.map((identifier) => ({
|
||||
value: identifier,
|
||||
title: (
|
||||
<div>
|
||||
{t(signUpIdentifierPhrase[identifier])}
|
||||
{identifier === SignUpIdentifier.None && (
|
||||
<span className={styles.socialOnlyDescription}>
|
||||
{t(
|
||||
'sign_in_exp.sign_up_and_sign_in.sign_up.social_only_creation_description'
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
onChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
onChange(value);
|
||||
postSignUpIdentifierChange(value);
|
||||
refreshSignInMethods();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ConnectorSetupWarning
|
||||
requiredConnectors={getSignUpRequiredConnectorTypes(signUpIdentifier)}
|
||||
/>
|
||||
<SignUpIdentifiersEditBox />
|
||||
</FormField>
|
||||
{signUpIdentifier !== SignUpIdentifier.None && (
|
||||
{showAuthenticationFields && (
|
||||
<FormField title="sign_in_exp.sign_up_and_sign_in.sign_up.sign_up_authentication">
|
||||
<FormFieldDescription>
|
||||
{t('sign_in_exp.sign_up_and_sign_in.sign_up.authentication_description')}
|
||||
|
@ -178,21 +57,12 @@ function SignUpForm() {
|
|||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_up.set_a_password_option')}
|
||||
disabled={!isDevFeaturesEnabled && isUsernamePasswordSignUp}
|
||||
checked={value}
|
||||
tooltip={conditional(
|
||||
!isDevFeaturesEnabled &&
|
||||
isUsernamePasswordSignUp &&
|
||||
t('sign_in_exp.sign_up_and_sign_in.tip.set_a_password')
|
||||
)}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
refreshSignInMethods();
|
||||
}}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{isVerificationRequiredSignUpIdentifiers(signUpIdentifier) && (
|
||||
{signUpVerify && (
|
||||
<Controller
|
||||
name="signUp.verify"
|
||||
control={control}
|
||||
|
@ -201,11 +71,8 @@ function SignUpForm() {
|
|||
disabled
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_up.verify_at_sign_up_option')}
|
||||
checked={value}
|
||||
tooltip={t('sign_in_exp.sign_up_and_sign_in.tip.verify_at_sign_up')}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
refreshSignInMethods();
|
||||
}}
|
||||
suffixTooltip={t('sign_in_exp.sign_up_and_sign_in.sign_up.verification_tip')}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import PageMeta from '@/components/PageMeta';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
|
||||
import SignInExperienceTabWrapper from '../components/SignInExperienceTabWrapper';
|
||||
|
||||
import AdvancedOptions from './AdvancedOptions';
|
||||
import SignInForm from './SignInForm';
|
||||
import SignUpForm from './SignUpForm';
|
||||
import NewSignUpFrom from './SignUpForm/SignUpForm';
|
||||
import SignUpFrom from './SignUpForm';
|
||||
import SocialSignInForm from './SocialSignInForm';
|
||||
|
||||
type Props = {
|
||||
|
@ -19,7 +17,7 @@ function SignUpAndSignIn({ isActive }: Props) {
|
|||
{isActive && (
|
||||
<PageMeta titleKey={['sign_in_exp.tabs.sign_up_and_sign_in', 'sign_in_exp.page_title']} />
|
||||
)}
|
||||
{isDevFeaturesEnabled ? <NewSignUpFrom /> : <SignUpForm />}
|
||||
<SignUpFrom />
|
||||
<SignInForm />
|
||||
<SocialSignInForm />
|
||||
<AdvancedOptions />
|
||||
|
|
|
@ -5,11 +5,6 @@ import {
|
|||
type SignUpIdentifier as SignUpIdentifierMethod,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import { type SignUpIdentifier } from '../../types';
|
||||
import { signUpIdentifiersMapping } from '../constants';
|
||||
|
||||
import { identifierRequiredConnectorMapping } from './constants';
|
||||
|
||||
export const createSignInMethod = (identifier: SignInIdentifier) => ({
|
||||
identifier,
|
||||
password: true,
|
||||
|
@ -17,33 +12,6 @@ export const createSignInMethod = (identifier: SignInIdentifier) => ({
|
|||
isPasswordPrimary: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Check if the verification is required for the given sign-up identifier.
|
||||
*
|
||||
* - Email
|
||||
* - Phone
|
||||
* - EmailOrSms
|
||||
*/
|
||||
export const isVerificationRequiredSignUpIdentifiers = (signUpIdentifier: SignUpIdentifier) => {
|
||||
const identifiers = signUpIdentifiersMapping[signUpIdentifier];
|
||||
|
||||
return (
|
||||
identifiers.includes(SignInIdentifier.Email) || identifiers.includes(SignInIdentifier.Phone)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* TODO: replace with the new implementation, once the multi sign-up identifier feature is fully implemented.
|
||||
*/
|
||||
export const getSignUpRequiredConnectorTypes = (
|
||||
signUpIdentifier: SignUpIdentifier
|
||||
): ConnectorType[] =>
|
||||
signUpIdentifiersMapping[signUpIdentifier]
|
||||
.map((identifier) => identifierRequiredConnectorMapping[identifier])
|
||||
// eslint-disable-next-line unicorn/prefer-native-coercion-functions
|
||||
.filter((connectorType): connectorType is ConnectorType => Boolean(connectorType));
|
||||
|
||||
export const getSignUpIdentifiersRequiredConnectors = (
|
||||
signUpIdentifiers: SignUpIdentifierMethod[]
|
||||
): ConnectorType[] => {
|
||||
|
|
|
@ -2,7 +2,6 @@ import { SignInIdentifier, type SignInExperience } from '@logto/schemas';
|
|||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
|
||||
import { signUpFormDataParser } from '../utils/parser';
|
||||
|
@ -18,10 +17,6 @@ function PasswordDisabledNotification({ after, className }: Props) {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const shouldShowNotification = useMemo(() => {
|
||||
if (!isDevFeaturesEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
identifiers.length === 1 &&
|
||||
identifiers[0]?.identifier === SignInIdentifier.Username &&
|
||||
|
|
|
@ -3,10 +3,6 @@ import { getSafe } from '@silverhand/essentials';
|
|||
import { diff } from 'deep-object-diff';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
|
||||
import type { SignUpForm } from '../../../types';
|
||||
import { signUpIdentifierPhrase } from '../../constants';
|
||||
import { signUpFormDataParser } from '../../utils/parser';
|
||||
|
||||
import DiffSegment from './DiffSegment';
|
||||
|
@ -18,54 +14,7 @@ type Props = {
|
|||
readonly isAfter?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* TODO: replace with NewSignUpDiffSection after dev features are ready
|
||||
*/
|
||||
function SignUpDiffSection({ before, after, isAfter = false }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const parsedBefore = signUpFormDataParser.fromSignUp(before);
|
||||
const parsedAfter = signUpFormDataParser.fromSignUp(after);
|
||||
const signUpDiff = isAfter ? diff(parsedBefore, parsedAfter) : diff(parsedAfter, parsedBefore);
|
||||
const signUp = isAfter ? parsedAfter : parsedBefore;
|
||||
const hasChanged = (path: keyof SignUpForm) => getSafe(signUpDiff, path) !== undefined;
|
||||
|
||||
const { identifier, password, verify } = signUp;
|
||||
const hasAuthentication = password || verify;
|
||||
const needConjunction = password && verify;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.title}>{t('sign_in_exp.save_alert.sign_up')}</div>
|
||||
<ul className={styles.list}>
|
||||
<li>
|
||||
<DiffSegment hasChanged={hasChanged('identifier')} isAfter={isAfter}>
|
||||
<DynamicT forKey={signUpIdentifierPhrase[identifier]} />
|
||||
</DiffSegment>
|
||||
{hasAuthentication && ' ('}
|
||||
{password && (
|
||||
<DiffSegment hasChanged={hasChanged('password')} isAfter={isAfter}>
|
||||
{t('sign_in_exp.sign_up_and_sign_in.sign_up.set_a_password_option')}
|
||||
</DiffSegment>
|
||||
)}
|
||||
{needConjunction && ` ${t('sign_in_exp.sign_up_and_sign_in.and')} `}
|
||||
{verify && (
|
||||
<DiffSegment hasChanged={hasChanged('verify')} isAfter={isAfter}>
|
||||
{needConjunction
|
||||
? t(
|
||||
'sign_in_exp.sign_up_and_sign_in.sign_up.verify_at_sign_up_option'
|
||||
).toLowerCase()
|
||||
: t('sign_in_exp.sign_up_and_sign_in.sign_up.verify_at_sign_up_option')}
|
||||
</DiffSegment>
|
||||
)}
|
||||
{hasAuthentication && ')'}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NewSignUpDiffSection({ before, after, isAfter = false }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const parsedBefore = signUpFormDataParser.fromSignUp(before);
|
||||
const parsedAfter = signUpFormDataParser.fromSignUp(after);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import type { SignInExperience } from '@logto/schemas';
|
||||
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
|
||||
import SignInDiffSection from './SignInDiffSection';
|
||||
import SignUpDiffSection, { NewSignUpDiffSection } from './SignUpDiffSection';
|
||||
import SignUpDiffSection from './SignUpDiffSection';
|
||||
import SocialTargetsDiffSection from './SocialTargetsDiffSection';
|
||||
|
||||
type Props = {
|
||||
|
@ -15,11 +13,7 @@ type Props = {
|
|||
function SignUpAndSignInDiffSection({ before, after, isAfter = false }: Props) {
|
||||
return (
|
||||
<>
|
||||
{isDevFeaturesEnabled ? (
|
||||
<NewSignUpDiffSection before={before.signUp} after={after.signUp} isAfter={isAfter} />
|
||||
) : (
|
||||
<SignUpDiffSection before={before.signUp} after={after.signUp} isAfter={isAfter} />
|
||||
)}
|
||||
<SignUpDiffSection before={before.signUp} after={after.signUp} isAfter={isAfter} />
|
||||
<SignInDiffSection
|
||||
before={before.signIn.methods}
|
||||
after={after.signIn.methods}
|
||||
|
|
|
@ -1,24 +1,10 @@
|
|||
import type { AdminConsoleKey } from '@logto/phrases';
|
||||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import { SignUpIdentifier } from '../types';
|
||||
|
||||
export const signUpIdentifiers = Object.values(SignUpIdentifier);
|
||||
import { type SignUpIdentifier } from '../types';
|
||||
|
||||
export const signInIdentifiers = Object.values(SignInIdentifier);
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* TODO: remove this once the multi sign-up identifier feature is fully implemented.
|
||||
*/
|
||||
export const signUpIdentifiersMapping: { [key in SignUpIdentifier]: SignInIdentifier[] } = {
|
||||
[SignUpIdentifier.Username]: [SignInIdentifier.Username],
|
||||
[SignUpIdentifier.Email]: [SignInIdentifier.Email],
|
||||
[SignUpIdentifier.Phone]: [SignInIdentifier.Phone],
|
||||
[SignUpIdentifier.EmailOrSms]: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
||||
[SignUpIdentifier.None]: [],
|
||||
};
|
||||
|
||||
type SignInIdentifierPhrase = {
|
||||
[key in SignInIdentifier]: AdminConsoleKey;
|
||||
};
|
||||
|
@ -32,11 +18,3 @@ export const signInIdentifierPhrase = Object.freeze({
|
|||
type SignUpIdentifierPhrase = {
|
||||
[key in SignUpIdentifier]: AdminConsoleKey;
|
||||
};
|
||||
|
||||
export const signUpIdentifierPhrase = Object.freeze({
|
||||
[SignUpIdentifier.Email]: 'sign_in_exp.sign_up_and_sign_in.identifiers_email',
|
||||
[SignUpIdentifier.Phone]: 'sign_in_exp.sign_up_and_sign_in.identifiers_phone',
|
||||
[SignUpIdentifier.Username]: 'sign_in_exp.sign_up_and_sign_in.identifiers_username',
|
||||
[SignUpIdentifier.EmailOrSms]: 'sign_in_exp.sign_up_and_sign_in.identifiers_email_or_sms',
|
||||
[SignUpIdentifier.None]: 'sign_in_exp.sign_up_and_sign_in.identifiers_none',
|
||||
}) satisfies SignUpIdentifierPhrase;
|
||||
|
|
|
@ -3,7 +3,6 @@ import { diff } from 'deep-object-diff';
|
|||
import type { DeepRequired, FieldErrorsImpl } from 'react-hook-form';
|
||||
|
||||
import type { SignInExperienceForm, SignInMethod, SignInMethodsObject } from '../../types';
|
||||
import { SignUpIdentifier } from '../../types';
|
||||
|
||||
export const convertToSignInMethodsObject = (signInMethods: SignInMethod[]): SignInMethodsObject =>
|
||||
signInMethods.reduce<SignInMethodsObject>(
|
||||
|
@ -54,16 +53,12 @@ export const getSignUpAndSignInErrorCount = (
|
|||
errors: FieldErrorsImpl<DeepRequired<SignInExperienceForm>>,
|
||||
formData: SignInExperienceForm
|
||||
) => {
|
||||
const signUpIdentifier = formData.signUp.identifier;
|
||||
/**
|
||||
* Note: we treat the `emailOrSms` sign-up identifier as 2 errors when it's invalid.
|
||||
*/
|
||||
const signUpIdentifierRelatedErrorCount =
|
||||
signUpIdentifier === SignUpIdentifier.EmailOrSms ? 2 : 1;
|
||||
|
||||
const { signUp, signIn } = errors;
|
||||
|
||||
const signUpErrorCount = signUp?.identifier ? signUpIdentifierRelatedErrorCount : 0;
|
||||
const signUpIdentifiersError = signUp?.identifiers;
|
||||
const signUpErrorCount = Array.isArray(signUpIdentifiersError)
|
||||
? signUpIdentifiersError.filter(Boolean).length
|
||||
: 0;
|
||||
|
||||
const signInMethodErrors = signIn?.methods;
|
||||
|
||||
|
|
|
@ -6,33 +6,16 @@ import {
|
|||
type SignInExperience,
|
||||
type SignUp,
|
||||
} from '@logto/schemas';
|
||||
import { conditional, isSameArray } from '@silverhand/essentials';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { emptyBranding } from '@/types/sign-in-experience';
|
||||
import { removeFalsyValues } from '@/utils/object';
|
||||
|
||||
import {
|
||||
type SignUpIdentifier,
|
||||
type UpdateSignInExperienceData,
|
||||
type SignInExperienceForm,
|
||||
type SignUpForm,
|
||||
} from '../../types';
|
||||
import { signUpIdentifiersMapping } from '../constants';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* TODO: remove this once the multi sign-up identifier feature is fully implemented.
|
||||
*/
|
||||
const mapIdentifiersToSignUpIdentifier = (identifiers: SignInIdentifier[]): SignUpIdentifier => {
|
||||
for (const [signUpIdentifier, mappedIdentifiers] of Object.entries(signUpIdentifiersMapping)) {
|
||||
if (isSameArray(identifiers, mappedIdentifiers)) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return signUpIdentifier as SignUpIdentifier;
|
||||
}
|
||||
}
|
||||
throw new Error('Invalid identifiers in the sign up settings.');
|
||||
};
|
||||
|
||||
/**
|
||||
* For backward compatibility,
|
||||
|
@ -115,23 +98,15 @@ export const signUpFormDataParser = {
|
|||
const { identifiers, secondaryIdentifiers, ...signUpData } = data;
|
||||
|
||||
return {
|
||||
identifier: mapIdentifiersToSignUpIdentifier(identifiers),
|
||||
identifiers: signUpIdentifiersParser.toSignUpForm(identifiers, secondaryIdentifiers),
|
||||
...signUpData,
|
||||
};
|
||||
},
|
||||
toSignUp: (formData: SignUpForm): SignUp => {
|
||||
const { identifier, identifiers, ...signUpFormData } = formData;
|
||||
|
||||
if (isDevFeaturesEnabled) {
|
||||
return {
|
||||
...signUpIdentifiersParser.toSieData(identifiers),
|
||||
...signUpFormData,
|
||||
};
|
||||
}
|
||||
const { identifiers, ...signUpFormData } = formData;
|
||||
|
||||
return {
|
||||
identifiers: signUpIdentifiersMapping[identifier],
|
||||
...signUpIdentifiersParser.toSieData(identifiers),
|
||||
...signUpFormData,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -25,11 +25,6 @@ export enum SignUpIdentifier {
|
|||
}
|
||||
|
||||
export type SignUpForm = Omit<SignUp, 'identifiers' | 'secondaryIdentifiers'> & {
|
||||
/**
|
||||
* TODO: remove this field after the multi sign-up identifier feature is fully implemented.
|
||||
* @deprecated
|
||||
*/
|
||||
identifier: SignUpIdentifier;
|
||||
/**
|
||||
* New identifiers field that merges the `signUpIdentifier` and `secondaryIdentifiers` fields
|
||||
**/
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
VerificationType,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import { EnvSet } from '#src/env-set/index.js';
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import type Libraries from '#src/tenants/Libraries.js';
|
||||
import type Queries from '#src/tenants/Queries.js';
|
||||
|
@ -173,27 +172,23 @@ export class SignInExperienceValidator {
|
|||
mandatoryUserProfile.add(mandatoryPrimaryIdentifier);
|
||||
}
|
||||
|
||||
// TODO: Remove this dev feature check
|
||||
// Check for mandatory secondary identifiers
|
||||
if (EnvSet.values.isDevFeaturesEnabled) {
|
||||
for (const { identifier } of secondaryIdentifiers) {
|
||||
switch (identifier) {
|
||||
case SignInIdentifier.Email: {
|
||||
mandatoryUserProfile.add(MissingProfile.email);
|
||||
continue;
|
||||
}
|
||||
case SignInIdentifier.Phone: {
|
||||
mandatoryUserProfile.add(MissingProfile.phone);
|
||||
continue;
|
||||
}
|
||||
case SignInIdentifier.Username: {
|
||||
mandatoryUserProfile.add(MissingProfile.username);
|
||||
continue;
|
||||
}
|
||||
case AlternativeSignUpIdentifier.EmailOrPhone: {
|
||||
mandatoryUserProfile.add(MissingProfile.emailOrPhone);
|
||||
continue;
|
||||
}
|
||||
for (const { identifier } of secondaryIdentifiers) {
|
||||
switch (identifier) {
|
||||
case SignInIdentifier.Email: {
|
||||
mandatoryUserProfile.add(MissingProfile.email);
|
||||
continue;
|
||||
}
|
||||
case SignInIdentifier.Phone: {
|
||||
mandatoryUserProfile.add(MissingProfile.phone);
|
||||
continue;
|
||||
}
|
||||
case SignInIdentifier.Username: {
|
||||
mandatoryUserProfile.add(MissingProfile.username);
|
||||
continue;
|
||||
}
|
||||
case AlternativeSignUpIdentifier.EmailOrPhone: {
|
||||
mandatoryUserProfile.add(MissingProfile.emailOrPhone);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,9 @@
|
|||
},
|
||||
"verify": {
|
||||
"description": "Whether the user is required to verify their email/phone when signing-up."
|
||||
},
|
||||
"secondaryIdentifiers": {
|
||||
"description": "Additional identifiers required during sign-up. Once specified, users will be prompted to provide these identifiers when creating an account."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import type { VerificationCodeIdentifier } from '@logto/schemas';
|
||||
import { InteractionEvent, VerificationType } from '@logto/schemas';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { useLocation, useSearchParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import UserInteractionContext from '@/Providers/UserInteractionContextProvider/UserInteractionContext';
|
||||
import { updateProfileWithVerificationCode } from '@/apis/experience';
|
||||
import { getInteractionEventFromState } from '@/apis/utils';
|
||||
import { isDevFeaturesEnabled } from '@/constants/env';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import type { ErrorHandlers } from '@/hooks/use-error-handler';
|
||||
import useErrorHandler from '@/hooks/use-error-handler';
|
||||
|
@ -27,6 +26,7 @@ const useContinueFlowCodeVerification = (
|
|||
) => {
|
||||
const [searchParameters] = useSearchParams();
|
||||
const redirectTo = useGlobalRedirectTo();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { state } = useLocation();
|
||||
const { verificationIdsMap } = useContext(UserInteractionContext);
|
||||
|
@ -64,18 +64,22 @@ const useContinueFlowCodeVerification = (
|
|||
|
||||
const { type, value } = identifier;
|
||||
|
||||
// TODO: remove this dev feature check after the feature is fully implemented
|
||||
// This is to ensure a consistent user experience during the registration process.
|
||||
// If email or phone number has been enabled as additional sign-up identifiers,
|
||||
// and user trying to provide an email/phone number that already exists in the system,
|
||||
// prompt the user to sign in with the existing identifier.
|
||||
// @see {user-register-flow-code-verification.ts} for more details.
|
||||
if (
|
||||
isDevFeaturesEnabled &&
|
||||
interactionEvent === InteractionEvent.Register &&
|
||||
isVerificationCodeEnabledForSignIn(type)
|
||||
) {
|
||||
showSignInWithExistIdentifierConfirmModal({ identifier, verificationId });
|
||||
showSignInWithExistIdentifierConfirmModal({
|
||||
identifier,
|
||||
verificationId,
|
||||
onCanceled: () => {
|
||||
navigate(-1);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import { useContext, useMemo, useCallback } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import PageContext from '@/Providers/PageContextProvider/PageContext';
|
||||
import { isDevFeaturesEnabled } from '@/constants/env';
|
||||
// eslint-disable-next-line unused-imports/no-unused-imports -- type only import
|
||||
import type useRequiredProfileErrorHandler from '@/hooks/use-required-profile-error-handler';
|
||||
import { type SignInExperienceResponse, type VerificationCodeIdentifier } from '@/types';
|
||||
|
@ -70,17 +69,11 @@ export const useSieMethods = (): UseSieMethodsReturnType => {
|
|||
return false;
|
||||
}
|
||||
|
||||
// TODO: Remove this check when the feature is enabled for all environments
|
||||
if (isDevFeaturesEnabled) {
|
||||
// Return true if the identifier is enabled for sign-up either as a primary or secondary identifier
|
||||
return (
|
||||
signUpMethods.includes(type) ||
|
||||
secondaryIdentifiers.includes(type) ||
|
||||
secondaryIdentifiers.includes(AlternativeSignUpIdentifier.EmailOrPhone)
|
||||
);
|
||||
}
|
||||
|
||||
return signUpMethods.includes(type);
|
||||
return (
|
||||
signUpMethods.includes(type) ||
|
||||
secondaryIdentifiers.includes(type) ||
|
||||
secondaryIdentifiers.includes(AlternativeSignUpIdentifier.EmailOrPhone)
|
||||
);
|
||||
},
|
||||
[secondaryIdentifiers, signUpMethods, experienceSettings]
|
||||
);
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser, generateNewUserProfile, UserApiTest } from '#src/helpers/user.js';
|
||||
import { devFeatureTest, generateUsername } from '#src/utils.js';
|
||||
import { generateUsername } from '#src/utils.js';
|
||||
|
||||
const verificationIdentifierType: readonly [SignInIdentifier.Email, SignInIdentifier.Phone] =
|
||||
Object.freeze([SignInIdentifier.Email, SignInIdentifier.Phone]);
|
||||
|
@ -106,140 +106,137 @@ describe('register new user with username and password', () => {
|
|||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe(
|
||||
'register new user with username and password with secondary identifiers',
|
||||
() => {
|
||||
beforeAll(async () => {
|
||||
await Promise.all([setEmailConnector(), setSmsConnector()]);
|
||||
});
|
||||
describe('register new user with username and password with secondary identifiers', () => {
|
||||
beforeAll(async () => {
|
||||
await Promise.all([setEmailConnector(), setSmsConnector()]);
|
||||
});
|
||||
|
||||
it.each([SignInIdentifier.Email, AlternativeSignUpIdentifier.EmailOrPhone])(
|
||||
'set %s as secondary identifier',
|
||||
async (secondaryIdentifier) => {
|
||||
await enableAllVerificationCodeSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: true,
|
||||
secondaryIdentifiers: [
|
||||
{
|
||||
identifier: secondaryIdentifier,
|
||||
verify: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { username, password, primaryEmail } = generateNewUserProfile({
|
||||
username: true,
|
||||
password: true,
|
||||
primaryEmail: true,
|
||||
});
|
||||
|
||||
const client = await initExperienceClient({
|
||||
interactionEvent: InteractionEvent.Register,
|
||||
});
|
||||
|
||||
await client.updateProfile({ type: SignInIdentifier.Username, value: username });
|
||||
await client.updateProfile({ type: 'password', value: password });
|
||||
|
||||
await expectRejects(client.identifyUser(), {
|
||||
status: 422,
|
||||
code: 'user.missing_profile',
|
||||
});
|
||||
|
||||
await fulfillUserEmail(client, primaryEmail);
|
||||
|
||||
await client.identifyUser();
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const userId = await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
|
||||
await signInWithPassword({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Username,
|
||||
value: username,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
await signInWithPassword({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Email,
|
||||
value: primaryEmail,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
await deleteUser(userId);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(verificationIdentifierType)(
|
||||
'should fail to sign-up with existing %s as secondary identifier, and directly sign-in instead',
|
||||
async (identifierType) => {
|
||||
await enableAllVerificationCodeSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: true,
|
||||
secondaryIdentifiers: [
|
||||
{
|
||||
identifier: AlternativeSignUpIdentifier.EmailOrPhone,
|
||||
verify: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { userProfile, user } = await generateNewUser({
|
||||
[identifiersTypeToUserProfile[identifierType]]: true,
|
||||
username: true,
|
||||
password: true,
|
||||
});
|
||||
|
||||
const client = await initExperienceClient({
|
||||
interactionEvent: InteractionEvent.Register,
|
||||
});
|
||||
|
||||
await client.updateProfile({ type: SignInIdentifier.Username, value: generateUsername() });
|
||||
|
||||
const identifier: VerificationCodeIdentifier = {
|
||||
type: identifierType,
|
||||
value: userProfile[identifiersTypeToUserProfile[identifierType]]!,
|
||||
};
|
||||
|
||||
const { verificationId, code } = await successfullySendVerificationCode(client, {
|
||||
identifier,
|
||||
interactionEvent: InteractionEvent.Register,
|
||||
});
|
||||
|
||||
await successfullyVerifyVerificationCode(client, {
|
||||
identifier,
|
||||
verificationId,
|
||||
code,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.identifyUser({
|
||||
verificationId,
|
||||
}),
|
||||
it.each([SignInIdentifier.Email, AlternativeSignUpIdentifier.EmailOrPhone])(
|
||||
'set %s as secondary identifier',
|
||||
async (secondaryIdentifier) => {
|
||||
await enableAllVerificationCodeSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: true,
|
||||
secondaryIdentifiers: [
|
||||
{
|
||||
code: `user.${identifierType}_already_in_use`,
|
||||
status: 422,
|
||||
}
|
||||
);
|
||||
identifier: secondaryIdentifier,
|
||||
verify: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await client.updateInteractionEvent({
|
||||
interactionEvent: InteractionEvent.SignIn,
|
||||
});
|
||||
const { username, password, primaryEmail } = generateNewUserProfile({
|
||||
username: true,
|
||||
password: true,
|
||||
primaryEmail: true,
|
||||
});
|
||||
|
||||
await client.identifyUser({
|
||||
const client = await initExperienceClient({
|
||||
interactionEvent: InteractionEvent.Register,
|
||||
});
|
||||
|
||||
await client.updateProfile({ type: SignInIdentifier.Username, value: username });
|
||||
await client.updateProfile({ type: 'password', value: password });
|
||||
|
||||
await expectRejects(client.identifyUser(), {
|
||||
status: 422,
|
||||
code: 'user.missing_profile',
|
||||
});
|
||||
|
||||
await fulfillUserEmail(client, primaryEmail);
|
||||
|
||||
await client.identifyUser();
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const userId = await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
|
||||
await signInWithPassword({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Username,
|
||||
value: username,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
await signInWithPassword({
|
||||
identifier: {
|
||||
type: SignInIdentifier.Email,
|
||||
value: primaryEmail,
|
||||
},
|
||||
password,
|
||||
});
|
||||
|
||||
await deleteUser(userId);
|
||||
}
|
||||
);
|
||||
|
||||
it.each(verificationIdentifierType)(
|
||||
'should fail to sign-up with existing %s as secondary identifier, and directly sign-in instead',
|
||||
async (identifierType) => {
|
||||
await enableAllVerificationCodeSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: true,
|
||||
secondaryIdentifiers: [
|
||||
{
|
||||
identifier: AlternativeSignUpIdentifier.EmailOrPhone,
|
||||
verify: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { userProfile, user } = await generateNewUser({
|
||||
[identifiersTypeToUserProfile[identifierType]]: true,
|
||||
username: true,
|
||||
password: true,
|
||||
});
|
||||
|
||||
const client = await initExperienceClient({
|
||||
interactionEvent: InteractionEvent.Register,
|
||||
});
|
||||
|
||||
await client.updateProfile({ type: SignInIdentifier.Username, value: generateUsername() });
|
||||
|
||||
const identifier: VerificationCodeIdentifier = {
|
||||
type: identifierType,
|
||||
value: userProfile[identifiersTypeToUserProfile[identifierType]]!,
|
||||
};
|
||||
|
||||
const { verificationId, code } = await successfullySendVerificationCode(client, {
|
||||
identifier,
|
||||
interactionEvent: InteractionEvent.Register,
|
||||
});
|
||||
|
||||
await successfullyVerifyVerificationCode(client, {
|
||||
identifier,
|
||||
verificationId,
|
||||
code,
|
||||
});
|
||||
|
||||
await expectRejects(
|
||||
client.identifyUser({
|
||||
verificationId,
|
||||
});
|
||||
}),
|
||||
{
|
||||
code: `user.${identifierType}_already_in_use`,
|
||||
status: 422,
|
||||
}
|
||||
);
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
await client.updateInteractionEvent({
|
||||
interactionEvent: InteractionEvent.SignIn,
|
||||
});
|
||||
|
||||
await deleteUser(user.id);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
await client.identifyUser({
|
||||
verificationId,
|
||||
});
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
|
||||
await deleteUser(user.id);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import { expectRejects } from '#src/helpers/index.js';
|
||||
import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js';
|
||||
import { devFeatureTest, generateEmail, generatePhone } from '#src/utils.js';
|
||||
import { generateEmail, generatePhone } from '#src/utils.js';
|
||||
|
||||
const verificationIdentifierType: readonly [SignInIdentifier.Email, SignInIdentifier.Phone] =
|
||||
Object.freeze([SignInIdentifier.Email, SignInIdentifier.Phone]);
|
||||
|
@ -198,7 +198,7 @@ describe('Register interaction with verification code happy path', () => {
|
|||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe('username as secondary identifier', () => {
|
||||
describe('username as secondary identifier', () => {
|
||||
beforeAll(async () => {
|
||||
await updateSignInExperience({
|
||||
signUp: {
|
||||
|
|
|
@ -1,20 +1,6 @@
|
|||
/* eslint-disable max-lines */
|
||||
import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js';
|
||||
import {
|
||||
expectModalWithTitle,
|
||||
expectToClickModalAction,
|
||||
expectToClickNavTab,
|
||||
expectToSaveChanges,
|
||||
goToAdminConsole,
|
||||
waitForToast,
|
||||
} from '#src/ui-helpers/index.js';
|
||||
import {
|
||||
expectNavigation,
|
||||
appendPathname,
|
||||
devFeatureDisabledTest,
|
||||
devFeatureTest,
|
||||
waitFor,
|
||||
} from '#src/utils.js';
|
||||
import { expectToClickNavTab, goToAdminConsole } from '#src/ui-helpers/index.js';
|
||||
import { expectNavigation, appendPathname, waitFor } from '#src/utils.js';
|
||||
|
||||
import { expectToSaveSignInExperience, waitForFormCard } from '../helpers.js';
|
||||
|
||||
|
@ -31,13 +17,9 @@ import {
|
|||
cleanUpSignInAndSignUpIdentifiers,
|
||||
expectToAddSignInMethod,
|
||||
expectToAddSignUpMethod,
|
||||
expectToAddSocialSignInConnector,
|
||||
expectToClickSignInMethodAuthnOption,
|
||||
expectToClickSignUpAuthnOption,
|
||||
expectToRemoveSignInMethod,
|
||||
expectToRemoveSocialSignInConnector,
|
||||
expectToResetSignUpAndSignInConfig,
|
||||
expectToSelectSignUpIdentifier,
|
||||
expectToSwapSignInMethodAuthnOption,
|
||||
resetSignUpAndSignInConfigToUsernamePassword,
|
||||
} from './helpers.js';
|
||||
|
@ -80,531 +62,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
await waitForFormCard(page, 'SOCIAL SIGN-IN');
|
||||
});
|
||||
|
||||
devFeatureDisabledTest.describe('email as sign-up identifier (verify only)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
||||
it('select email as sign-up method and disable password settings for sign-up', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Email address');
|
||||
// Disable password settings for sign-up
|
||||
await expectToClickSignUpAuthnOption(page, 'Create your password');
|
||||
// Username will be added in later tests
|
||||
await expectToRemoveSignInMethod(page, 'Username');
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password + verification code
|
||||
*/
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('update email sign-in method', async () => {
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Password',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code + password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Password',
|
||||
});
|
||||
await expectToSwapSignInMethodAuthnOption(page, 'Email address');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('add username sign-in method', async () => {
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code + password
|
||||
* - Username: password
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Username');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('add & update phone number sign-in method', async () => {
|
||||
await expectToAddSignInMethod(page, 'Phone number');
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code + password
|
||||
* - Username: password
|
||||
* - Phone number: password + verification code
|
||||
*/
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code + password
|
||||
* - Username: password
|
||||
* - Phone number: verification code
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Password',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code + password
|
||||
* - Phone number: verification code
|
||||
*/
|
||||
await expectToRemoveSignInMethod(page, 'Username');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code + password
|
||||
* - Phone number: password + verification code
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Password',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureDisabledTest.describe('email as sign-up identifier (password & verify)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
||||
it('select email as sign-in method and enable password settings for sign-up', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Email address');
|
||||
// Username will be added in later tests
|
||||
await expectToRemoveSignInMethod(page, 'Username');
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password + verification code
|
||||
*/
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('update email sign-in method', async () => {
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code + password
|
||||
*/
|
||||
// Sign-in method: Email address + verification code + password
|
||||
await expectToSwapSignInMethodAuthnOption(page, 'Email address');
|
||||
await expectToSaveSignInExperience(page);
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Verification code',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('add phone number & username as sign-in method', async () => {
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
* - Phone number: password + verification code
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Phone number');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
* - Phone number: password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Verification code',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
* - Phone number: password
|
||||
* - Username: password
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Username');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
* - Phone number: password + verification code
|
||||
* - Username: password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Verification code',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureDisabledTest.describe('phone as sign-up identifier (verify only)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
||||
it('select email as sign-in method and disable password settings for sign-up', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Phone number');
|
||||
// Disable password settings for sign-up
|
||||
await expectToClickSignUpAuthnOption(page, 'Create your password');
|
||||
// Username will be added in later tests
|
||||
await expectToRemoveSignInMethod(page, 'Username');
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: password + verification code
|
||||
*/
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('update sign-in methods', async () => {
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: verification code + password
|
||||
*/
|
||||
await expectToSwapSignInMethodAuthnOption(page, 'Phone number');
|
||||
await expectToSaveSignInExperience(page);
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: verification code
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Password',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: verification code
|
||||
* - Email address: password + verification code
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Email address');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: verification code
|
||||
* - Email address: verification code
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Password',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: verification code
|
||||
* - Email address: verification code
|
||||
* - Username: password
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Username');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureDisabledTest.describe('phone as sign-up identifier (password & verify)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
||||
it('select email as sign-in method and enable password settings for sign-up', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Phone number');
|
||||
// Username will be added in later tests
|
||||
await expectToRemoveSignInMethod(page, 'Username');
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: password + verification code
|
||||
*/
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('update sign-in methods', async () => {
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: verification code + password
|
||||
*/
|
||||
await expectToSwapSignInMethodAuthnOption(page, 'Phone number');
|
||||
await expectToSaveSignInExperience(page);
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Verification code',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Phone number: password
|
||||
* - Username: password
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Username');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureDisabledTest.describe('email or phone as sign-up identifier (verify only)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
||||
it('select email or phone as sign-up identifier and disable password settings for sign-up', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Email address or phone number');
|
||||
await expectToClickSignUpAuthnOption(page, 'Create your password');
|
||||
// Username will be added in later tests
|
||||
await expectToRemoveSignInMethod(page, 'Username');
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password + verification code
|
||||
* - Phone number: password + verification code
|
||||
*/
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('update sign-in method configs', async () => {
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code + password
|
||||
* - Phone number: verification code + password
|
||||
*/
|
||||
await expectToSwapSignInMethodAuthnOption(page, 'Email address');
|
||||
await expectToSwapSignInMethodAuthnOption(page, 'Phone number');
|
||||
await expectToSaveSignInExperience(page);
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code
|
||||
* - Phone number: verification code + password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Password',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code
|
||||
* - Phone number: verification code
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Password',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code
|
||||
* - Phone number: verification code
|
||||
* - Username: password
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Username');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureDisabledTest.describe(
|
||||
'email or phone as sign-up identifier (password & verify)',
|
||||
() => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
||||
it('select email or phone as sign-up identifier and enable password settings for sign-up', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Email address or phone number');
|
||||
// Username will be added in later tests
|
||||
await expectToRemoveSignInMethod(page, 'Username');
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password + verification code
|
||||
* - Phone number: password + verification code
|
||||
*/
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('update sign-in method configs', async () => {
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code + password
|
||||
* - Phone number: verification code + password
|
||||
*/
|
||||
await expectToSwapSignInMethodAuthnOption(page, 'Email address');
|
||||
await expectToSwapSignInMethodAuthnOption(page, 'Phone number');
|
||||
await expectToSaveSignInExperience(page);
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
* - Phone number: verification code + password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Verification code',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
* - Phone number: password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Verification code',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
* - Phone number: password
|
||||
* - Username: password
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Username');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
devFeatureDisabledTest.describe('not applicable as sign-up identifier', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
||||
it('select not applicable as sign-up identifier', async () => {
|
||||
await expectToSelectSignUpIdentifier(
|
||||
page,
|
||||
'Not applicable(This apply to social only account creation)'
|
||||
);
|
||||
await expectToRemoveSignInMethod(page, 'Username');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('update sign-in methods', async () => {
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password + verification code
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Email address', false);
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Verification code',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Verification code',
|
||||
});
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Password',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code
|
||||
* - Phone number: password verification code
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Phone number');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code
|
||||
* - Phone number: password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Verification code',
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: verification code
|
||||
* - Phone number: password
|
||||
* - Username: password
|
||||
*/
|
||||
await expectToAddSignInMethod(page, 'Username');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('add social sign-in connector', async () => {
|
||||
await expectToAddSocialSignInConnector(page, 'Apple');
|
||||
// Should have diffs about social sign-in connector
|
||||
await expectToSaveChanges(page);
|
||||
await expectModalWithTitle(page, 'Reminder');
|
||||
|
||||
const beforeSection = await expect(page).toMatchElement(
|
||||
'div[class$=section]:has(div[class$=title])',
|
||||
{ text: 'Before' }
|
||||
);
|
||||
|
||||
// Ensure no social-related content in the "Before" section. The modal is already visible, so
|
||||
// the timeout can be short.
|
||||
await expect(beforeSection).not.toMatchElement('div[class$=title]', {
|
||||
text: 'Social',
|
||||
timeout: 50,
|
||||
});
|
||||
|
||||
// Have social content in the after section
|
||||
const afterSection = await expect(page).toMatchElement(
|
||||
'div[class$=section]:has(div[class$=title])',
|
||||
{ text: 'After' }
|
||||
);
|
||||
await expect(afterSection).toMatchElement('div[class$=title]', {
|
||||
text: 'Social',
|
||||
});
|
||||
|
||||
await expectToClickModalAction(page, 'Confirm');
|
||||
await waitForToast(page, { text: 'Saved' });
|
||||
|
||||
// Reset
|
||||
await expectToRemoveSocialSignInConnector(page, 'Apple');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe('email as sign-up identifier', () => {
|
||||
describe('email as sign-up identifier', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanUpSignInAndSignUpIdentifiers(page);
|
||||
});
|
||||
|
@ -696,7 +154,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe('email as sign-up identifier (password and verify)', () => {
|
||||
describe('email as sign-up identifier (password and verify)', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanUpSignInAndSignUpIdentifiers(page);
|
||||
});
|
||||
|
@ -775,7 +233,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe('email or phone as sign-up identifier (verify only', () => {
|
||||
describe('email or phone as sign-up identifier (verify only', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanUpSignInAndSignUpIdentifiers(page);
|
||||
});
|
||||
|
@ -837,7 +295,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe('email or phone as sign-up identifier (password & verify)', () => {
|
||||
describe('email or phone as sign-up identifier (password & verify)', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanUpSignInAndSignUpIdentifiers(page);
|
||||
});
|
||||
|
@ -894,7 +352,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe('username and email as sign-up identifier', () => {
|
||||
describe('username and email as sign-up identifier', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanUpSignInAndSignUpIdentifiers(page);
|
||||
});
|
||||
|
@ -931,7 +389,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
devFeatureTest.describe('username and email or phone as sign-up identifier', () => {
|
||||
describe('username and email or phone as sign-up identifier', () => {
|
||||
beforeAll(async () => {
|
||||
await cleanUpSignInAndSignUpIdentifiers(page);
|
||||
});
|
||||
|
@ -999,4 +457,3 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
await expectToSaveSignInExperience(page);
|
||||
});
|
||||
});
|
||||
/* eslint-enable max-lines */
|
||||
|
|
|
@ -1,30 +1,8 @@
|
|||
import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js';
|
||||
import {
|
||||
expectToClickNavTab,
|
||||
goToAdminConsole,
|
||||
expectToSaveChanges,
|
||||
} from '#src/ui-helpers/index.js';
|
||||
import { expectNavigation, appendPathname, devFeatureDisabledTest } from '#src/utils.js';
|
||||
import { expectToClickNavTab, goToAdminConsole } from '#src/ui-helpers/index.js';
|
||||
import { expectNavigation, appendPathname } from '#src/utils.js';
|
||||
|
||||
import { expectToSaveSignInExperience, waitForFormCard } from '../helpers.js';
|
||||
|
||||
import {
|
||||
expectToDeletePasswordlessConnector,
|
||||
expectToSetupPasswordlessConnector,
|
||||
testSendgridConnector,
|
||||
testTwilioConnector,
|
||||
} from './connector-setup-helpers.js';
|
||||
import {
|
||||
expectToSelectSignUpIdentifier,
|
||||
expectNotificationInField,
|
||||
expectSignUpIdentifierSelectorError,
|
||||
expectToAddSignInMethod,
|
||||
expectSignInMethodError,
|
||||
expectErrorsOnNavTab,
|
||||
expectToClickSignUpAuthnOption,
|
||||
expectToClickSignInMethodAuthnOption,
|
||||
expectToResetSignUpAndSignInConfig,
|
||||
} from './helpers.js';
|
||||
import { waitForFormCard } from '../helpers.js';
|
||||
|
||||
await page.setViewport({ width: 1920, height: 1080 });
|
||||
|
||||
|
@ -50,258 +28,4 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
|
|||
await waitForFormCard(page, 'SIGN IN');
|
||||
await waitForFormCard(page, 'SOCIAL SIGN-IN');
|
||||
});
|
||||
|
||||
devFeatureDisabledTest.describe('cases that no connector is setup', () => {
|
||||
describe('email address as sign-up identifier', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page, false);
|
||||
});
|
||||
|
||||
it('should fail to setup email as sign-up identifier', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Email address');
|
||||
// Disable password settings for sign-up settings
|
||||
await expectToClickSignUpAuthnOption(page, 'Create your password');
|
||||
|
||||
await expectNotificationInField(page, {
|
||||
field: 'Sign-up identifier',
|
||||
content: /No email connector set-up yet./,
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignUpIdentifierSelectorError(page);
|
||||
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '1 error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to add phone number sign-in method', async () => {
|
||||
await expectToAddSignInMethod(page, 'Phone number');
|
||||
await expectNotificationInField(page, {
|
||||
field: 'Identifier and authentication settings for sign-in',
|
||||
content: /No SMS connector set-up yet./,
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignInMethodError(page, 'Phone number');
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '2 errors',
|
||||
});
|
||||
|
||||
// Disable password option for sign-in method
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Password',
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignInMethodError(page, 'Phone number');
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '2 errors',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('phone number as sign-up identifier', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page, false);
|
||||
});
|
||||
|
||||
it('should fail to setup phone number as sign-up identifier', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Phone number');
|
||||
// Disable password settings for sign-up settings
|
||||
await expectToClickSignUpAuthnOption(page, 'Create your password');
|
||||
|
||||
await expectNotificationInField(page, {
|
||||
field: 'Sign-up identifier',
|
||||
content: /No SMS connector set-up yet./,
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignUpIdentifierSelectorError(page);
|
||||
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '1 error',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to add email address sign-in method', async () => {
|
||||
await expectToAddSignInMethod(page, 'Email address');
|
||||
await expectNotificationInField(page, {
|
||||
field: 'Identifier and authentication settings for sign-in',
|
||||
content: /No email connector set-up yet./,
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignInMethodError(page, 'Email address');
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '2 errors',
|
||||
});
|
||||
|
||||
// Disable password option for sign-in method
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Password',
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignInMethodError(page, 'Email address');
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '2 errors',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('social sign-in', () => {
|
||||
it('should display no social connector notification in social sign-in field', async () => {
|
||||
await expectNotificationInField(page, {
|
||||
field: 'Social sign-in',
|
||||
content: /You haven’t set up any social connector yet./,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureDisabledTest.describe('cases that only Email connector is setup', () => {
|
||||
beforeAll(async () => {
|
||||
// Email connector
|
||||
await expectToSetupPasswordlessConnector(page, testSendgridConnector);
|
||||
// Nav back to sign-in experience page
|
||||
await expectNavigation(
|
||||
page.goto(
|
||||
appendPathname('/console/sign-in-experience/sign-up-and-sign-in', logtoConsoleUrl).href
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await expectToDeletePasswordlessConnector(page, testSendgridConnector);
|
||||
await expectNavigation(
|
||||
page.goto(
|
||||
appendPathname('/console/sign-in-experience/sign-up-and-sign-in', logtoConsoleUrl).href
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
describe('email address as sign-up identifier', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
||||
it('should setup email as sign-up identifier', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Email address');
|
||||
// Disable password settings for sign-up settings
|
||||
await expectToClickSignUpAuthnOption(page, 'Create your password');
|
||||
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('should fail to add phone number sign-in method', async () => {
|
||||
await expectToAddSignInMethod(page, 'Phone number');
|
||||
await expectNotificationInField(page, {
|
||||
field: 'Identifier and authentication settings for sign-in',
|
||||
content: /No SMS connector set-up yet./,
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignInMethodError(page, 'Phone number');
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '1 error',
|
||||
});
|
||||
|
||||
// Disable password option for sign-in method
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Password',
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignInMethodError(page, 'Phone number');
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '1 error',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
devFeatureDisabledTest.describe('cases that only SMS connector is setup', () => {
|
||||
beforeAll(async () => {
|
||||
// SMS connector
|
||||
await expectToSetupPasswordlessConnector(page, testTwilioConnector);
|
||||
// Nav back to sign-in experience page
|
||||
await expectNavigation(
|
||||
page.goto(
|
||||
appendPathname('/console/sign-in-experience/sign-up-and-sign-in', logtoConsoleUrl).href
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await expectToDeletePasswordlessConnector(page, testTwilioConnector);
|
||||
await expectNavigation(
|
||||
page.goto(
|
||||
appendPathname('/console/sign-in-experience/sign-up-and-sign-in', logtoConsoleUrl).href
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
describe('phone number as sign-up identifier', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
||||
it('should setup phone number as sign-up identifier', async () => {
|
||||
await expectToSelectSignUpIdentifier(page, 'Phone number');
|
||||
// Disable password settings for sign-up settings
|
||||
await expectToClickSignUpAuthnOption(page, 'Create your password');
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
});
|
||||
|
||||
it('should fail to add email sign-in method', async () => {
|
||||
await expectToAddSignInMethod(page, 'Email address');
|
||||
await expectNotificationInField(page, {
|
||||
field: 'Identifier and authentication settings for sign-in',
|
||||
content: /No email connector set-up yet./,
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignInMethodError(page, 'Email address');
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '1 error',
|
||||
});
|
||||
|
||||
// Disable password option for sign-in method
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Email address',
|
||||
option: 'Password',
|
||||
});
|
||||
|
||||
await expectToSaveChanges(page);
|
||||
|
||||
await expectSignInMethodError(page, 'Email address');
|
||||
await expectErrorsOnNavTab(page, {
|
||||
tab: 'Sign-up and sign-in',
|
||||
error: '1 error',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue