mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor(console): refactor SIE settings form (#7154)
* refactor(core): remove mandatory sign-in password verification rule remove mandatory sign-in password verification rule * refactor(console): refactor SIE settings form refactor SIE settings form * feat(console): add dev feature guard * feat(console): add multi sign-up identifiers (#7168) update SIE page to support multi sign-up identifiers * chore(console): disable test cases for devFeature disable test cases for devFeature * refactor(test): disable some console tests for devFeature disable some console tests for devFeature * fix(console): fix emailOrPhone sign-in method sync fix emailOrPhone sign-in method sync
This commit is contained in:
parent
e6f315d1b4
commit
cdc1acb238
21 changed files with 821 additions and 190 deletions
|
@ -3,13 +3,18 @@ import { conditional } from '@silverhand/essentials';
|
|||
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 { identifierRequiredConnectorMapping } from '../../constants';
|
||||
import { getSignUpRequiredConnectorTypes, createSignInMethod } from '../../utils';
|
||||
import {
|
||||
getSignUpRequiredConnectorTypes,
|
||||
createSignInMethod,
|
||||
getSignUpIdentifiersRequiredConnectors,
|
||||
} from '../../utils';
|
||||
|
||||
import AddButton from './AddButton';
|
||||
import SignInMethodItem from './SignInMethodItem';
|
||||
|
@ -43,12 +48,17 @@ function SignInMethodEditBox() {
|
|||
|
||||
const {
|
||||
identifier: signUpIdentifier,
|
||||
identifiers: signUpIdentifiers,
|
||||
password: isSignUpPasswordRequired,
|
||||
verify: isSignUpVerificationRequired,
|
||||
} = signUp;
|
||||
|
||||
const requiredSignInIdentifiers = signUpIdentifiersMapping[signUpIdentifier];
|
||||
const ignoredWarningConnectors = getSignUpRequiredConnectorTypes(signUpIdentifier);
|
||||
|
||||
// TODO: Remove this dev feature guard when multi sign-up identifiers are launched
|
||||
const ignoredWarningConnectors = isDevFeaturesEnabled
|
||||
? getSignUpIdentifiersRequiredConnectors(signUpIdentifiers.map(({ identifier }) => identifier))
|
||||
: getSignUpRequiredConnectorTypes(signUpIdentifier);
|
||||
|
||||
const signInIdentifierOptions = signInIdentifiers.filter((candidateIdentifier) =>
|
||||
fields.every(({ identifier }) => identifier !== candidateIdentifier)
|
||||
|
@ -103,12 +113,15 @@ function SignInMethodEditBox() {
|
|||
<SignInMethodItem
|
||||
signInMethod={value}
|
||||
isPasswordCheckable={
|
||||
identifier !== SignInIdentifier.Username && !isSignUpPasswordRequired
|
||||
identifier !== SignInIdentifier.Username &&
|
||||
(isDevFeaturesEnabled || !isSignUpPasswordRequired)
|
||||
}
|
||||
isVerificationCodeCheckable={
|
||||
!(isSignUpVerificationRequired && !isSignUpPasswordRequired)
|
||||
}
|
||||
isDeletable={!requiredSignInIdentifiers.includes(identifier)}
|
||||
isDeletable={
|
||||
isDevFeaturesEnabled || !requiredSignInIdentifiers.includes(identifier)
|
||||
}
|
||||
requiredConnectors={requiredConnectors}
|
||||
hasError={Boolean(error)}
|
||||
errorMessage={error?.message}
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
import { AlternativeSignUpIdentifier, SignInIdentifier } from '@logto/schemas';
|
||||
import { useEffect, 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 { createSignInMethod } from '../utils';
|
||||
|
||||
import SignUpIdentifiersEditBox from './SignUpIdentifiersEditBox';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
function SignUpForm() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
control,
|
||||
setValue,
|
||||
getValues,
|
||||
trigger,
|
||||
formState: { submitCount, dirtyFields },
|
||||
} = useFormContext<SignInExperienceForm>();
|
||||
|
||||
// Note: `useWatch` is a hook that returns the updated value on every render.
|
||||
// Unlike `watch`, it doesn't require a re-render to get the updated value (alway return the current ref).
|
||||
const signUp = useWatch({
|
||||
control,
|
||||
name: 'signUp',
|
||||
});
|
||||
|
||||
const signUpIdentifiers = useWatch({
|
||||
control,
|
||||
name: 'signUp.identifiers',
|
||||
});
|
||||
|
||||
const { shouldShowAuthenticationFields, shouldShowVerificationField } = useMemo(() => {
|
||||
return {
|
||||
shouldShowAuthenticationFields: signUpIdentifiers.length > 0,
|
||||
shouldShowVerificationField: signUpIdentifiers[0]?.identifier !== SignInIdentifier.Username,
|
||||
};
|
||||
}, [signUpIdentifiers]);
|
||||
|
||||
// Should sync the sign-up identifier auth settings when the sign-up identifiers changed
|
||||
// TODO: need to check with designer
|
||||
useEffect(() => {
|
||||
// Only trigger the effect when the identifiers field is dirty
|
||||
const isIdentifiersDirty = dirtyFields.signUp?.identifiers;
|
||||
if (!isIdentifiersDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const identifiers = signUpIdentifiers.map(({ identifier }) => identifier);
|
||||
if (identifiers.length === 0) {
|
||||
setValue('signUp.password', false);
|
||||
setValue('signUp.verify', false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (identifiers.includes(SignInIdentifier.Username)) {
|
||||
setValue('signUp.password', true);
|
||||
}
|
||||
|
||||
// Disable verification when the primary identifier is username,
|
||||
// otherwise enable it for the rest of the identifiers (email, phone, emailOrPhone)
|
||||
setValue('signUp.verify', identifiers[0] !== SignInIdentifier.Username);
|
||||
}, [dirtyFields.signUp?.identifiers, setValue, signUpIdentifiers]);
|
||||
|
||||
// Sync sign-in methods when sign-up methods change
|
||||
useEffect(() => {
|
||||
// Only trigger the effect when the sign-up field is dirty
|
||||
const isIdentifiersDirty = dirtyFields.signUp;
|
||||
if (!isIdentifiersDirty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signInMethods = getValues('signIn.methods');
|
||||
const { password, identifiers } = signUp;
|
||||
|
||||
const enabledSignUpIdentifiers = identifiers.reduce<SignInIdentifier[]>(
|
||||
(identifiers, { identifier: signUpIdentifier }) => {
|
||||
if (signUpIdentifier === AlternativeSignUpIdentifier.EmailOrPhone) {
|
||||
return [...identifiers, SignInIdentifier.Email, SignInIdentifier.Phone];
|
||||
}
|
||||
|
||||
return [...identifiers, signUpIdentifier];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Note: 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, signUpIdentifier) => {
|
||||
if (signInMethods.some(({ identifier }) => identifier === signUpIdentifier)) {
|
||||
return methods;
|
||||
}
|
||||
|
||||
return [...methods, createSignInMethod(signUpIdentifier)];
|
||||
}, signInMethods);
|
||||
|
||||
setValue(
|
||||
'signIn.methods',
|
||||
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,
|
||||
// Note: if password is not set for sign-up,
|
||||
// then auto enable verification code for email and phone sign-in methods.
|
||||
verificationCode: password ? method.verificationCode : true,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Note: we need to revalidate the sign-in methods after the signIn form data has been updated
|
||||
if (submitCount) {
|
||||
// Wait for the form re-render before validating the new data.
|
||||
setTimeout(() => {
|
||||
void trigger('signIn.methods');
|
||||
}, 0);
|
||||
}
|
||||
}, [dirtyFields.signUp, getValues, setValue, signUp, submitCount, trigger]);
|
||||
|
||||
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>
|
||||
{shouldShowAuthenticationFields && (
|
||||
<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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{shouldShowVerificationField && (
|
||||
<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}
|
||||
tooltip={t('sign_in_exp.sign_up_and_sign_in.tip.verify_at_sign_up')}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FormField>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignUpForm;
|
|
@ -0,0 +1,35 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.signUpMethodItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: _.unit(2) 0;
|
||||
gap: _.unit(2);
|
||||
}
|
||||
|
||||
.signUpMethod {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
padding: _.unit(3) _.unit(2);
|
||||
background-color: var(--color-layer-2);
|
||||
border-radius: 8px;
|
||||
cursor: move;
|
||||
gap: _.unit(1);
|
||||
color: var(--color-text);
|
||||
font: var(--font-label-2);
|
||||
|
||||
&.error {
|
||||
outline: 1px solid var(--color-error);
|
||||
}
|
||||
|
||||
.draggableIcon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-error);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import {
|
||||
AlternativeSignUpIdentifier,
|
||||
type ConnectorType,
|
||||
type SignUpIdentifier,
|
||||
} from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Draggable from '@/assets/icons/draggable.svg?react';
|
||||
import Minus from '@/assets/icons/minus.svg?react';
|
||||
import IconButton from '@/ds-components/IconButton';
|
||||
|
||||
import ConnectorSetupWarning from '../../components/ConnectorSetupWarning';
|
||||
|
||||
import styles from './SignUpIdentifierItem.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly identifier: SignUpIdentifier;
|
||||
readonly requiredConnectors: ConnectorType[];
|
||||
readonly hasError?: boolean;
|
||||
readonly errorMessage?: string;
|
||||
readonly onDelete: () => void;
|
||||
};
|
||||
|
||||
function SignUpIdentifierItem({
|
||||
identifier,
|
||||
requiredConnectors,
|
||||
hasError,
|
||||
errorMessage,
|
||||
onDelete,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div key={identifier} className={styles.signUpMethodItem}>
|
||||
<div className={classNames(styles.signUpMethod, hasError && styles.error)}>
|
||||
<Draggable className={styles.draggableIcon} />
|
||||
{t(
|
||||
`sign_in_exp.sign_up_and_sign_in.identifiers_${
|
||||
identifier === AlternativeSignUpIdentifier.EmailOrPhone ? 'email_or_sms' : identifier
|
||||
}`
|
||||
)}
|
||||
</div>
|
||||
<IconButton onClick={onDelete}>
|
||||
<Minus />
|
||||
</IconButton>
|
||||
</div>
|
||||
{errorMessage && <div className={styles.errorMessage}>{errorMessage}</div>}
|
||||
<ConnectorSetupWarning requiredConnectors={requiredConnectors} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignUpIdentifierItem;
|
|
@ -0,0 +1,3 @@
|
|||
.draggleItemContainer {
|
||||
transform: translate(0, 0);
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
import {
|
||||
AlternativeSignUpIdentifier,
|
||||
SignInIdentifier,
|
||||
type SignUpIdentifier,
|
||||
} from '@logto/schemas';
|
||||
import { t } from 'i18next';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
|
||||
|
||||
import { DragDropProvider, DraggableItem } from '@/ds-components/DragDrop';
|
||||
import useEnabledConnectorTypes from '@/hooks/use-enabled-connector-types';
|
||||
import { type SignInExperienceForm } from '@/pages/SignInExperience/types';
|
||||
|
||||
import IdentifiersAddButton from '../../components/IdentifiersAddButton';
|
||||
import { getSignUpIdentifiersRequiredConnectors } from '../../utils';
|
||||
|
||||
import SignUpIdentifierItem from './SignUpIdentifierItem';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const signInIdentifierOptions = Object.values(SignInIdentifier).map((identifier) => ({
|
||||
value: identifier,
|
||||
label: t(`admin_console.sign_in_exp.sign_up_and_sign_in.identifiers_${identifier}`),
|
||||
}));
|
||||
|
||||
const emailOrPhoneOption = {
|
||||
value: AlternativeSignUpIdentifier.EmailOrPhone,
|
||||
label: t('admin_console.sign_in_exp.sign_up_and_sign_in.identifiers_email_or_sms'),
|
||||
};
|
||||
|
||||
const signUpIdentifierOptions = [...signInIdentifierOptions, emailOrPhoneOption];
|
||||
|
||||
function SignUpIdentifiersEditBox() {
|
||||
const { control } = useFormContext<SignInExperienceForm>();
|
||||
|
||||
const signUpIdentifiers = useWatch({ control, name: 'signUp.identifiers' });
|
||||
|
||||
const { isConnectorTypeEnabled } = useEnabledConnectorTypes();
|
||||
|
||||
const { fields, swap, remove, append } = useFieldArray({
|
||||
control,
|
||||
name: 'signUp.identifiers',
|
||||
});
|
||||
|
||||
const options = useMemo<
|
||||
Array<{
|
||||
value: SignUpIdentifier;
|
||||
label: string;
|
||||
}>
|
||||
>(() => {
|
||||
const identifiersSet = new Set(signUpIdentifiers.map(({ identifier }) => identifier));
|
||||
|
||||
return signUpIdentifierOptions.filter(({ value }) => {
|
||||
// Basic condition: filter out if identifiers include the value
|
||||
if (identifiersSet.has(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Condition 2: If identifiers include EmailOrPhone, filter out Email and Phone
|
||||
if (
|
||||
identifiersSet.has(AlternativeSignUpIdentifier.EmailOrPhone) &&
|
||||
(value === SignInIdentifier.Email || value === SignInIdentifier.Phone)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Condition 3: If identifiers include Email or Phone, filter out EmailOrPhone
|
||||
if (
|
||||
(identifiersSet.has(SignInIdentifier.Email) ||
|
||||
identifiersSet.has(SignInIdentifier.Phone)) &&
|
||||
value === AlternativeSignUpIdentifier.EmailOrPhone
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If none of the conditions matched, keep the value
|
||||
return true;
|
||||
});
|
||||
}, [signUpIdentifiers]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DragDropProvider>
|
||||
{fields.map((data, index) => {
|
||||
const { id, identifier } = data;
|
||||
const requiredConnectors = getSignUpIdentifiersRequiredConnectors([identifier]);
|
||||
|
||||
return (
|
||||
<DraggableItem
|
||||
key={id}
|
||||
id={id}
|
||||
sortIndex={index}
|
||||
moveItem={swap}
|
||||
className={styles.draggleItemContainer}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`signUp.identifiers.${index}`}
|
||||
rules={{
|
||||
validate: () => {
|
||||
if (
|
||||
requiredConnectors.some(
|
||||
(connectorType) => !isConnectorTypeEnabled(connectorType)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}}
|
||||
render={({
|
||||
field: {
|
||||
value: { identifier },
|
||||
},
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<SignUpIdentifierItem
|
||||
identifier={identifier}
|
||||
requiredConnectors={requiredConnectors}
|
||||
hasError={Boolean(error)}
|
||||
errorMessage={error?.message}
|
||||
onDelete={() => {
|
||||
remove(index);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DraggableItem>
|
||||
);
|
||||
})}
|
||||
</DragDropProvider>
|
||||
<IdentifiersAddButton
|
||||
type="sign-up"
|
||||
options={options}
|
||||
hasSelectedIdentifiers={signUpIdentifiers.length > 0}
|
||||
onSelected={(identifier) => {
|
||||
append({ identifier });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SignUpIdentifiersEditBox;
|
|
@ -1,7 +1,10 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useCallback } from 'react';
|
||||
import { Controller, useFormContext } 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';
|
||||
|
@ -19,11 +22,9 @@ import {
|
|||
} from '../../constants';
|
||||
import ConnectorSetupWarning from '../components/ConnectorSetupWarning';
|
||||
import {
|
||||
createSignInMethod,
|
||||
getSignUpRequiredConnectorTypes,
|
||||
isVerificationRequiredSignUpIdentifiers,
|
||||
createSignInMethod,
|
||||
getSignInMethodPasswordCheckState,
|
||||
getSignInMethodVerificationCodeCheckState,
|
||||
} from '../utils';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
@ -38,41 +39,42 @@ function SignUpForm() {
|
|||
trigger,
|
||||
formState: { errors, submitCount },
|
||||
} = useFormContext<SignInExperienceForm>();
|
||||
|
||||
const { isConnectorTypeEnabled } = useEnabledConnectorTypes();
|
||||
|
||||
const signUp = watch('signUp');
|
||||
|
||||
const { identifier: signUpIdentifier } = signUp;
|
||||
|
||||
const isUsernamePasswordSignUp = signUpIdentifier === SignUpIdentifier.Username;
|
||||
|
||||
const postSignUpIdentifierChange = (signUpIdentifier: SignUpIdentifier) => {
|
||||
if (signUpIdentifier === SignUpIdentifier.Username) {
|
||||
setValue('signUp.password', true);
|
||||
setValue('signUp.verify', false);
|
||||
const postSignUpIdentifierChange = useCallback(
|
||||
(signUpIdentifier: SignUpIdentifier) => {
|
||||
if (signUpIdentifier === SignUpIdentifier.Username) {
|
||||
setValue('signUp.password', true);
|
||||
setValue('signUp.verify', false);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
if (signUpIdentifier === SignUpIdentifier.None) {
|
||||
setValue('signUp.password', false);
|
||||
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);
|
||||
}
|
||||
};
|
||||
if (isVerificationRequiredSignUpIdentifiers(signUpIdentifier)) {
|
||||
setValue('signUp.verify', true);
|
||||
}
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
const refreshSignInMethods = () => {
|
||||
const signInMethods = getValues('signIn.methods');
|
||||
const { identifier: signUpIdentifier } = signUp;
|
||||
const { verify, password, identifier } = signUp;
|
||||
const enabledSignUpIdentifiers = signUpIdentifiersMapping[identifier];
|
||||
|
||||
// Note: append required sign-in methods according to the sign-up identifier config
|
||||
const requiredSignInIdentifiers = signUpIdentifiersMapping[signUpIdentifier];
|
||||
const allSignInMethods = requiredSignInIdentifiers.reduce((methods, requiredIdentifier) => {
|
||||
// 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;
|
||||
}
|
||||
|
@ -80,20 +82,26 @@ function SignUpForm() {
|
|||
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
|
||||
allSignInMethods.map((method) => {
|
||||
const { identifier, password, verificationCode } = method;
|
||||
mergedSignInMethods.map((method) => {
|
||||
const { identifier } = method;
|
||||
|
||||
if (identifier === SignInIdentifier.Username) {
|
||||
return method;
|
||||
}
|
||||
|
||||
return {
|
||||
...method,
|
||||
password: getSignInMethodPasswordCheckState(identifier, signUp, password),
|
||||
verificationCode: getSignInMethodVerificationCodeCheckState(
|
||||
identifier,
|
||||
signUp,
|
||||
verificationCode
|
||||
),
|
||||
// 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,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
@ -170,10 +178,11 @@ function SignUpForm() {
|
|||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_up.set_a_password_option')}
|
||||
disabled={isUsernamePasswordSignUp}
|
||||
disabled={!isDevFeaturesEnabled && isUsernamePasswordSignUp}
|
||||
checked={value}
|
||||
tooltip={conditional(
|
||||
isUsernamePasswordSignUp &&
|
||||
!isDevFeaturesEnabled &&
|
||||
isUsernamePasswordSignUp &&
|
||||
t('sign_in_exp.sign_up_and_sign_in.tip.set_a_password')
|
||||
)}
|
||||
onChange={(value) => {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.addAnotherIdentifierDropdown {
|
||||
min-width: 208px;
|
||||
}
|
||||
|
||||
.addIdentifierDropDown {
|
||||
min-width: unset;
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import type { SignInIdentifier, SignUpIdentifier } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import CirclePlus from '@/assets/icons/circle-plus.svg?react';
|
||||
import Plus from '@/assets/icons/plus.svg?react';
|
||||
import ActionMenu from '@/ds-components/ActionMenu';
|
||||
import type { Props as ButtonProps } from '@/ds-components/Button';
|
||||
import { DropdownItem } from '@/ds-components/Dropdown';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
type MethodsType = 'sign-in' | 'sign-up';
|
||||
|
||||
type Options<T> = Array<{
|
||||
value: T;
|
||||
label: string;
|
||||
}>;
|
||||
|
||||
type Props<T> = {
|
||||
readonly type: MethodsType;
|
||||
readonly options: Options<T>;
|
||||
readonly onSelected: (identifier: T) => void;
|
||||
readonly hasSelectedIdentifiers: boolean;
|
||||
};
|
||||
|
||||
function IdentifiersAddButton<T extends SignInIdentifier | SignUpIdentifier>({
|
||||
type,
|
||||
options,
|
||||
onSelected,
|
||||
hasSelectedIdentifiers,
|
||||
}: Props<T>) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
if (options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const addSignInMethodButtonProps: ButtonProps = {
|
||||
type: 'default',
|
||||
size: 'medium',
|
||||
title: `sign_in_exp.sign_up_and_sign_in.sign_in.${
|
||||
type === 'sign-in' ? 'add_sign_in_method' : 'add_sign_up_method'
|
||||
}`,
|
||||
icon: <Plus className={styles.plusIcon} />,
|
||||
};
|
||||
|
||||
const addAnotherButtonProps: ButtonProps = {
|
||||
type: 'text',
|
||||
size: 'small',
|
||||
title: 'general.add_another',
|
||||
icon: <CirclePlus />,
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionMenu
|
||||
buttonProps={hasSelectedIdentifiers ? addAnotherButtonProps : addSignInMethodButtonProps}
|
||||
dropdownHorizontalAlign="start"
|
||||
dropdownClassName={classNames(
|
||||
hasSelectedIdentifiers ? styles.addAnotherIdentifierDropdown : styles.addIdentifierDropDown
|
||||
)}
|
||||
isDropdownFullWidth={!hasSelectedIdentifiers}
|
||||
>
|
||||
{options.map(({ value, label }) => (
|
||||
<DropdownItem
|
||||
key={value}
|
||||
onClick={() => {
|
||||
onSelected(value);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</ActionMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default IdentifiersAddButton;
|
|
@ -1,10 +1,12 @@
|
|||
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 SocialSignInForm from './SocialSignInForm';
|
||||
|
||||
type Props = {
|
||||
|
@ -17,7 +19,7 @@ function SignUpAndSignIn({ isActive }: Props) {
|
|||
{isActive && (
|
||||
<PageMeta titleKey={['sign_in_exp.tabs.sign_up_and_sign_in', 'sign_in_exp.page_title']} />
|
||||
)}
|
||||
<SignUpForm />
|
||||
{isDevFeaturesEnabled ? <NewSignUpFrom /> : <SignUpForm />}
|
||||
<SignInForm />
|
||||
<SocialSignInForm />
|
||||
<AdvancedOptions />
|
||||
|
|
|
@ -1,43 +1,15 @@
|
|||
import { type ConnectorType, SignInIdentifier } from '@logto/schemas';
|
||||
import {
|
||||
AlternativeSignUpIdentifier,
|
||||
ConnectorType,
|
||||
SignInIdentifier,
|
||||
type SignUpIdentifier as SignUpIdentifierMethod,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import type { SignUpForm } from '../../types';
|
||||
import { SignUpIdentifier } from '../../types';
|
||||
import { type SignUpIdentifier } from '../../types';
|
||||
import { signUpIdentifiersMapping } from '../constants';
|
||||
|
||||
import { identifierRequiredConnectorMapping } from './constants';
|
||||
|
||||
export const getSignInMethodPasswordCheckState = (
|
||||
signInIdentifier: SignInIdentifier,
|
||||
signUpConfig: SignUpForm,
|
||||
currentCheckState: boolean
|
||||
) => {
|
||||
if (signInIdentifier === SignInIdentifier.Username) {
|
||||
return currentCheckState;
|
||||
}
|
||||
|
||||
const { password: isSignUpPasswordRequired } = signUpConfig;
|
||||
|
||||
return isSignUpPasswordRequired || currentCheckState;
|
||||
};
|
||||
|
||||
export const getSignInMethodVerificationCodeCheckState = (
|
||||
signInIdentifier: SignInIdentifier,
|
||||
signUpConfig: SignUpForm,
|
||||
currentCheckState: boolean
|
||||
) => {
|
||||
if (signInIdentifier === SignInIdentifier.Username) {
|
||||
return currentCheckState;
|
||||
}
|
||||
|
||||
const { identifier: signUpIdentifier, password: isSignUpPasswordRequired } = signUpConfig;
|
||||
|
||||
if (SignUpIdentifier.None !== signUpIdentifier && !isSignUpPasswordRequired) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return currentCheckState;
|
||||
};
|
||||
|
||||
export const createSignInMethod = (identifier: SignInIdentifier) => ({
|
||||
identifier,
|
||||
password: true,
|
||||
|
@ -45,6 +17,13 @@ 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];
|
||||
|
||||
|
@ -53,6 +32,10 @@ export const isVerificationRequiredSignUpIdentifiers = (signUpIdentifier: SignUp
|
|||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* TODO: replace with the new implementation, once the multi sign-up identifier feature is fully implemented.
|
||||
*/
|
||||
export const getSignUpRequiredConnectorTypes = (
|
||||
signUpIdentifier: SignUpIdentifier
|
||||
): ConnectorType[] =>
|
||||
|
@ -60,3 +43,32 @@ export const getSignUpRequiredConnectorTypes = (
|
|||
.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[] => {
|
||||
const requiredConnectors = new Set<ConnectorType>();
|
||||
|
||||
for (const signUpIdentifier of signUpIdentifiers) {
|
||||
switch (signUpIdentifier) {
|
||||
case SignInIdentifier.Email: {
|
||||
requiredConnectors.add(ConnectorType.Email);
|
||||
continue;
|
||||
}
|
||||
case SignInIdentifier.Phone: {
|
||||
requiredConnectors.add(ConnectorType.Sms);
|
||||
continue;
|
||||
}
|
||||
case AlternativeSignUpIdentifier.EmailOrPhone: {
|
||||
requiredConnectors.add(ConnectorType.Email);
|
||||
requiredConnectors.add(ConnectorType.Sms);
|
||||
continue;
|
||||
}
|
||||
default: {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(requiredConnectors);
|
||||
};
|
||||
|
|
|
@ -7,6 +7,10 @@ export const signUpIdentifiers = Object.values(SignUpIdentifier);
|
|||
|
||||
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],
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
import { passwordPolicyGuard } from '@logto/core-kit';
|
||||
import {
|
||||
AlternativeSignUpIdentifier,
|
||||
SignInIdentifier,
|
||||
SignInMode,
|
||||
type SignInExperience,
|
||||
type SignUp,
|
||||
type SignInIdentifier,
|
||||
} from '@logto/schemas';
|
||||
import { isSameArray } from '@silverhand/essentials';
|
||||
import { conditional, isSameArray } 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,
|
||||
type SignUpIdentifier,
|
||||
} 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)) {
|
||||
|
@ -28,17 +34,101 @@ const mapIdentifiersToSignUpIdentifier = (identifiers: SignInIdentifier[]): Sign
|
|||
throw new Error('Invalid identifiers in the sign up settings.');
|
||||
};
|
||||
|
||||
/**
|
||||
* For backward compatibility,
|
||||
* we need to safely parse the @see {SignUp['identifiers']} to the @see {SignUpForm['identifiers']} format.
|
||||
*/
|
||||
const parsePrimaryIdentifier = (identifiers: SignInIdentifier[]): SignUpForm['identifiers'] => {
|
||||
if (identifiers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (identifiers.length === 1 && identifiers[0]) {
|
||||
return [
|
||||
{
|
||||
identifier: identifiers[0],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
identifiers.length === 2 &&
|
||||
identifiers.includes(SignInIdentifier.Email) &&
|
||||
identifiers.includes(SignInIdentifier.Phone)
|
||||
) {
|
||||
return [
|
||||
{
|
||||
identifier: AlternativeSignUpIdentifier.EmailOrPhone,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
throw new Error('Invalid identifiers in the sign up settings.');
|
||||
};
|
||||
|
||||
const signUpIdentifiersParser = {
|
||||
/**
|
||||
* Merge the @see {SignUp['identifiers']} with the @see {SignUp['secondaryIdentifiers']}
|
||||
* into one @see {SignUpForm['identifiers']} form field.
|
||||
*/
|
||||
toSignUpForm: (
|
||||
identifiers: SignInIdentifier[],
|
||||
secondaryIdentifiers: SignUp['secondaryIdentifiers'] = []
|
||||
): SignUpForm['identifiers'] => {
|
||||
const primarySignUpIdentifier = parsePrimaryIdentifier(identifiers);
|
||||
return [
|
||||
...primarySignUpIdentifier,
|
||||
...secondaryIdentifiers.map(({ identifier }) => ({ identifier })),
|
||||
];
|
||||
},
|
||||
/**
|
||||
* For backward compatibility,
|
||||
* we need to split the @see {SignUpForm['identifiers']} into @see {SignUp['identifiers']}
|
||||
* and @see {SignUp['secondaryIdentifiers']} two fields.
|
||||
*/
|
||||
toSieData: (
|
||||
signUpIdentifiers: SignUpForm['identifiers']
|
||||
): Pick<SignUp, 'identifiers' | 'secondaryIdentifiers'> => {
|
||||
const primaryIdentifier = signUpIdentifiers[0];
|
||||
|
||||
const identifiers = primaryIdentifier
|
||||
? primaryIdentifier.identifier === AlternativeSignUpIdentifier.EmailOrPhone
|
||||
? [SignInIdentifier.Email, SignInIdentifier.Phone]
|
||||
: [primaryIdentifier.identifier]
|
||||
: [];
|
||||
|
||||
const secondaryIdentifiers = signUpIdentifiers.slice(1).map(({ identifier }) => ({
|
||||
identifier,
|
||||
// For email or phone, we always set the `verify` flag to true.
|
||||
...conditional(identifier !== SignInIdentifier.Username && { verify: true }),
|
||||
}));
|
||||
|
||||
return {
|
||||
identifiers,
|
||||
secondaryIdentifiers,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const signUpFormDataParser = {
|
||||
fromSignUp: (data: SignUp): SignUpForm => {
|
||||
const { identifiers, ...signUpData } = data;
|
||||
const { identifiers, secondaryIdentifiers, ...signUpData } = data;
|
||||
|
||||
return {
|
||||
identifier: mapIdentifiersToSignUpIdentifier(identifiers),
|
||||
identifiers: signUpIdentifiersParser.toSignUpForm(identifiers, secondaryIdentifiers),
|
||||
...signUpData,
|
||||
};
|
||||
},
|
||||
toSignUp: (formData: SignUpForm): SignUp => {
|
||||
const { identifier, ...signUpFormData } = formData;
|
||||
const { identifier, identifiers, ...signUpFormData } = formData;
|
||||
|
||||
if (isDevFeaturesEnabled) {
|
||||
return {
|
||||
...signUpIdentifiersParser.toSieData(identifiers),
|
||||
...signUpFormData,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
identifiers: signUpIdentifiersMapping[identifier],
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { type PasswordPolicy } from '@logto/core-kit';
|
||||
import { type SignUp, type SignInExperience, type SignInIdentifier } from '@logto/schemas';
|
||||
import {
|
||||
type SignUp,
|
||||
type SignInExperience,
|
||||
type SignInIdentifier,
|
||||
type SignUpIdentifier as SignUpIdentifierMethod,
|
||||
} from '@logto/schemas';
|
||||
|
||||
export enum SignInExperienceTab {
|
||||
Branding = 'branding',
|
||||
|
@ -8,6 +13,9 @@ export enum SignInExperienceTab {
|
|||
PasswordPolicy = 'password-policy',
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export enum SignUpIdentifier {
|
||||
Email = 'email',
|
||||
Phone = 'phone',
|
||||
|
@ -16,8 +24,24 @@ export enum SignUpIdentifier {
|
|||
None = 'none',
|
||||
}
|
||||
|
||||
export type SignUpForm = Omit<SignUp, 'identifiers'> & {
|
||||
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
|
||||
**/
|
||||
identifiers: Array<{
|
||||
/**
|
||||
* Wrapped the identifier value into an object to make it manageable using the `useFieldArray` hook.
|
||||
* `useFieldArray` requires the array item to be an object.
|
||||
* Also for the future benefit, we may add `verify` field to the identifier object, once we support
|
||||
* unverified email/phone as the sign-up identifier.
|
||||
*/
|
||||
identifier: SignUpIdentifierMethod;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type SignInExperienceForm = Omit<
|
||||
|
|
|
@ -113,33 +113,6 @@ describe('validate sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('throws when sign up requires set a password and sign in password is not enabled', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
{
|
||||
methods: [
|
||||
{
|
||||
...mockSignInMethod,
|
||||
identifier: SignInIdentifier.Email,
|
||||
password: false,
|
||||
verificationCode: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
...mockSignUp,
|
||||
identifiers: [SignInIdentifier.Email],
|
||||
password: true,
|
||||
},
|
||||
enabledConnectors
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.password_sign_in_must_be_enabled',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when sign up only requires verify and sign in verification code is not enabled', () => {
|
||||
expect(() => {
|
||||
validateSignIn(
|
||||
|
|
|
@ -47,15 +47,6 @@ export const validateSignIn = (
|
|||
})
|
||||
);
|
||||
|
||||
if (signUp.password) {
|
||||
assertThat(
|
||||
signIn.methods.every(({ password }) => password),
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.password_sign_in_must_be_enabled',
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (signUp.verify && !signUp.password) {
|
||||
assertThat(
|
||||
signIn.methods.every(
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
goToAdminConsole,
|
||||
waitForToast,
|
||||
} from '#src/ui-helpers/index.js';
|
||||
import { expectNavigation, appendPathname } from '#src/utils.js';
|
||||
import { expectNavigation, appendPathname, devFeatureDisabledTest } from '#src/utils.js';
|
||||
|
||||
import { expectToSaveSignInExperience, waitForFormCard } from '../helpers.js';
|
||||
|
||||
|
@ -70,7 +70,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
await waitForFormCard(page, 'SOCIAL SIGN-IN');
|
||||
});
|
||||
|
||||
describe('email as sign-up identifier (verify only)', () => {
|
||||
devFeatureDisabledTest.describe('email as sign-up identifier (verify only)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
@ -165,7 +165,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('email as sign-up identifier (password & verify)', () => {
|
||||
devFeatureDisabledTest.describe('email as sign-up identifier (password & verify)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
@ -245,7 +245,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('phone as sign-up identifier (verify only)', () => {
|
||||
devFeatureDisabledTest.describe('phone as sign-up identifier (verify only)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
@ -312,7 +312,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('phone as sign-up identifier (password & verify)', () => {
|
||||
devFeatureDisabledTest.describe('phone as sign-up identifier (password & verify)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
@ -357,7 +357,7 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('email or phone as sign-up identifier (verify only)', () => {
|
||||
devFeatureDisabledTest.describe('email or phone as sign-up identifier (verify only)', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
|
@ -419,68 +419,71 @@ describe('sign-in experience(happy path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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',
|
||||
devFeatureDisabledTest.describe(
|
||||
'email or phone as sign-up identifier (password & verify)',
|
||||
() => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page);
|
||||
});
|
||||
await expectToSaveSignInExperience(page, { needToConfirmChanges: true });
|
||||
|
||||
/**
|
||||
* Sign-in method
|
||||
* - Email address: password
|
||||
* - Phone number: password
|
||||
*/
|
||||
await expectToClickSignInMethodAuthnOption(page, {
|
||||
method: 'Phone number',
|
||||
option: 'Verification code',
|
||||
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 });
|
||||
});
|
||||
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 });
|
||||
});
|
||||
});
|
||||
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);
|
||||
|
||||
describe('not applicable as sign-up identifier', () => {
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
goToAdminConsole,
|
||||
expectToSaveChanges,
|
||||
} from '#src/ui-helpers/index.js';
|
||||
import { expectNavigation, appendPathname } from '#src/utils.js';
|
||||
import { expectNavigation, appendPathname, devFeatureDisabledTest } from '#src/utils.js';
|
||||
|
||||
import { expectToSaveSignInExperience, waitForFormCard } from '../helpers.js';
|
||||
|
||||
|
@ -51,7 +51,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
|
|||
await waitForFormCard(page, 'SOCIAL SIGN-IN');
|
||||
});
|
||||
|
||||
describe('cases that no connector is setup', () => {
|
||||
devFeatureDisabledTest.describe('cases that no connector is setup', () => {
|
||||
describe('email address as sign-up identifier', () => {
|
||||
afterAll(async () => {
|
||||
await expectToResetSignUpAndSignInConfig(page, false);
|
||||
|
@ -174,7 +174,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('cases that only Email connector is setup', () => {
|
||||
devFeatureDisabledTest.describe('cases that only Email connector is setup', () => {
|
||||
beforeAll(async () => {
|
||||
// Email connector
|
||||
await expectToSetupPasswordlessConnector(page, testSendgridConnector);
|
||||
|
@ -240,7 +240,7 @@ describe('sign-in experience(sad path): sign-up and sign-in', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('cases that only SMS connector is setup', () => {
|
||||
devFeatureDisabledTest.describe('cases that only SMS connector is setup', () => {
|
||||
beforeAll(async () => {
|
||||
// SMS connector
|
||||
await expectToSetupPasswordlessConnector(page, testTwilioConnector);
|
||||
|
|
|
@ -132,3 +132,8 @@ export const devFeatureTest = Object.freeze({
|
|||
it: isDevFeaturesEnabled ? it : it.skip,
|
||||
describe: isDevFeaturesEnabled ? describe : describe.skip,
|
||||
});
|
||||
|
||||
export const devFeatureDisabledTest = Object.freeze({
|
||||
it: isDevFeaturesEnabled ? it.skip : it,
|
||||
describe: isDevFeaturesEnabled ? describe.skip : describe,
|
||||
});
|
||||
|
|
|
@ -8,9 +8,8 @@ const sign_up_and_sign_in = {
|
|||
or: 'or',
|
||||
sign_up: {
|
||||
title: 'SIGN UP',
|
||||
sign_up_identifier: 'Sign-up identifier',
|
||||
identifier_description:
|
||||
'The sign-up identifier is required for account creation and must be included in your sign-in screen.',
|
||||
sign_up_identifier: 'Sign-up identifiers',
|
||||
identifier_description: 'The sign-up identifier is required for account creation.',
|
||||
sign_up_authentication: 'Authentication setting for sign-up',
|
||||
authentication_description:
|
||||
'All selected actions will be obligatory for users to complete the flow.',
|
||||
|
@ -24,6 +23,7 @@ const sign_up_and_sign_in = {
|
|||
description:
|
||||
'Users can sign in using any of the options available. Adjust the layout by drag and dropping below options.',
|
||||
add_sign_in_method: 'Add sign-in method',
|
||||
add_sign_up_method: 'Add sign-up method',
|
||||
password_auth: 'Password',
|
||||
verification_code_auth: 'Verification code',
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.',
|
||||
|
|
|
@ -57,8 +57,10 @@ export enum AlternativeSignUpIdentifier {
|
|||
EmailOrPhone = 'emailOrPhone',
|
||||
}
|
||||
|
||||
export type SignUpIdentifier = SignInIdentifier | AlternativeSignUpIdentifier;
|
||||
|
||||
type RequiredSignUpIdentifierSettings = {
|
||||
identifier: SignInIdentifier | AlternativeSignUpIdentifier;
|
||||
identifier: SignUpIdentifier;
|
||||
/**
|
||||
* For `email` and `phone` identifiers only. If `true`, the user must verify the email or phone number.
|
||||
*/
|
||||
|
|
Loading…
Add table
Reference in a new issue