mirror of
https://github.com/logto-io/logto.git
synced 2025-03-17 22:31:28 -05:00
refactor(console): form validations on sign-up and sign-in page (#2448)
This commit is contained in:
parent
0af95d790b
commit
62b25dbf14
15 changed files with 286 additions and 312 deletions
|
@ -1,33 +1,13 @@
|
|||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FormField from '@/components/FormField';
|
||||
|
||||
import type { SignInExperienceForm } from '../../types';
|
||||
import SignInMethodEditBox from './components/SignInMethodEditBox';
|
||||
import {
|
||||
signUpIdentifierToRequiredConnectorMapping,
|
||||
signUpToSignInIdentifierMapping,
|
||||
} from './constants';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const SignInForm = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const { control, watch } = useFormContext<SignInExperienceForm>();
|
||||
|
||||
const signUpIdentifier = watch('signUp.identifier');
|
||||
const setupPasswordAtSignUp = watch('signUp.password');
|
||||
const setupVerificationAtSignUp = watch('signUp.verify');
|
||||
|
||||
if (
|
||||
!signUpIdentifier ||
|
||||
setupPasswordAtSignUp === undefined ||
|
||||
setupVerificationAtSignUp === undefined
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.title}>{t('sign_in_exp.sign_up_and_sign_in.sign_in.title')}</div>
|
||||
|
@ -35,25 +15,7 @@ const SignInForm = () => {
|
|||
<div className={styles.formFieldDescription}>
|
||||
{t('sign_in_exp.sign_up_and_sign_in.sign_in.description')}
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="signIn.methods"
|
||||
defaultValue={[]}
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return (
|
||||
<SignInMethodEditBox
|
||||
value={value}
|
||||
requiredSignInIdentifiers={signUpToSignInIdentifierMapping[signUpIdentifier]}
|
||||
ignoredWarningConnectors={
|
||||
signUpIdentifierToRequiredConnectorMapping[signUpIdentifier]
|
||||
}
|
||||
isSignUpPasswordRequired={setupPasswordAtSignUp}
|
||||
isSignUpVerificationRequired={setupVerificationAtSignUp}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<SignInMethodEditBox />
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -10,10 +10,15 @@ import useEnabledConnectorTypes from '@/hooks/use-enabled-connector-types';
|
|||
|
||||
import type { SignInExperienceForm } from '../../types';
|
||||
import ConnectorSetupWarning from './components/ConnectorSetupWarning';
|
||||
import {
|
||||
getSignInMethodPasswordCheckState,
|
||||
getSignInMethodVerificationCodeCheckState,
|
||||
} from './components/SignInMethodEditBox/utilities';
|
||||
import {
|
||||
requiredVerifySignUpIdentifiers,
|
||||
signUpIdentifiers,
|
||||
signUpIdentifierToRequiredConnectorMapping,
|
||||
signUpToSignInIdentifierMapping,
|
||||
} from './constants';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -22,12 +27,14 @@ const SignUpForm = () => {
|
|||
const {
|
||||
control,
|
||||
setValue,
|
||||
getValues,
|
||||
watch,
|
||||
formState: { errors },
|
||||
trigger,
|
||||
formState: { errors, submitCount },
|
||||
} = useFormContext<SignInExperienceForm>();
|
||||
const { isConnectorTypeEnabled } = useEnabledConnectorTypes();
|
||||
|
||||
const signUpIdentifier = watch('signUp.identifier');
|
||||
const { identifier: signUpIdentifier } = watch('signUp') ?? {};
|
||||
|
||||
if (!signUpIdentifier) {
|
||||
return null;
|
||||
|
@ -53,6 +60,53 @@ const SignUpForm = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const refreshSignInMethods = () => {
|
||||
const signUpIdentifier = getValues('signUp.identifier');
|
||||
const signInMethods = getValues('signIn.methods');
|
||||
const isSignUpPasswordRequired = getValues('signUp.password');
|
||||
|
||||
// Note: append required sign-in methods according to the sign-up identifier config
|
||||
const requiredSignInIdentifiers = signUpToSignInIdentifierMapping[signUpIdentifier];
|
||||
const allSignInMethods = requiredSignInIdentifiers.reduce((methods, requiredIdentifier) => {
|
||||
if (signInMethods.some(({ identifier }) => identifier === requiredIdentifier)) {
|
||||
return methods;
|
||||
}
|
||||
|
||||
return [
|
||||
...methods,
|
||||
{
|
||||
identifier: requiredIdentifier,
|
||||
password: getSignInMethodPasswordCheckState(requiredIdentifier, isSignUpPasswordRequired),
|
||||
verificationCode: getSignInMethodVerificationCodeCheckState(requiredIdentifier),
|
||||
isPasswordPrimary: true,
|
||||
},
|
||||
];
|
||||
}, signInMethods);
|
||||
|
||||
setValue(
|
||||
'signIn.methods',
|
||||
// Note: refresh sign-in authentications according to the sign-up authentications config
|
||||
allSignInMethods.map((method) => {
|
||||
const { identifier, password } = method;
|
||||
|
||||
return {
|
||||
...method,
|
||||
password: getSignInMethodPasswordCheckState(
|
||||
identifier,
|
||||
isSignUpPasswordRequired,
|
||||
password
|
||||
),
|
||||
verificationCode: getSignInMethodVerificationCodeCheckState(identifier),
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Note: we need to revalidate the sign-in methods after we have submitted
|
||||
if (submitCount) {
|
||||
void trigger('signIn.methods');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.title}>{t('sign_in_exp.sign_up_and_sign_in.sign_up.title')}</div>
|
||||
|
@ -65,10 +119,6 @@ const SignUpForm = () => {
|
|||
control={control}
|
||||
rules={{
|
||||
validate: (value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return signUpIdentifierToRequiredConnectorMapping[value].every((connectorType) =>
|
||||
isConnectorTypeEnabled(connectorType)
|
||||
);
|
||||
|
@ -101,6 +151,7 @@ const SignUpForm = () => {
|
|||
}
|
||||
onChange(value);
|
||||
postSignUpIdentifierChange(value);
|
||||
refreshSignInMethods();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -122,9 +173,12 @@ const SignUpForm = () => {
|
|||
<Checkbox
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_up.set_a_password_option')}
|
||||
disabled={signUpIdentifier === SignUpIdentifier.Username}
|
||||
value={value ?? false}
|
||||
value={value}
|
||||
disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.set_a_password')}
|
||||
onChange={onChange}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
refreshSignInMethods();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -135,10 +189,13 @@ const SignUpForm = () => {
|
|||
render={({ field: { value, onChange } }) => (
|
||||
<Checkbox
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_up.verify_at_sign_up_option')}
|
||||
value={value ?? false}
|
||||
value={value}
|
||||
disabled={requiredVerifySignUpIdentifiers.includes(signUpIdentifier)}
|
||||
disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.verify_at_sign_up')}
|
||||
onChange={onChange}
|
||||
onChange={(value) => {
|
||||
onChange(value);
|
||||
refreshSignInMethods();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { ConnectorType } from '@logto/schemas';
|
||||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
|
@ -10,6 +11,7 @@ import SwitchArrowIcon from '@/assets/images/switch-arrow.svg';
|
|||
import Checkbox from '@/components/Checkbox';
|
||||
import IconButton from '@/components/IconButton';
|
||||
|
||||
import ConnectorSetupWarning from '../ConnectorSetupWarning';
|
||||
import * as styles from './index.module.scss';
|
||||
import type { SignInMethod } from './types';
|
||||
|
||||
|
@ -18,13 +20,15 @@ type Props = {
|
|||
isPasswordCheckable: boolean;
|
||||
isVerificationCodeCheckable: boolean;
|
||||
isDeletable: boolean;
|
||||
requiredConnectors: ConnectorType[];
|
||||
hasError?: boolean;
|
||||
errorMessage?: string;
|
||||
onVerificationStateChange: (
|
||||
identifier: SignInIdentifier,
|
||||
verification: 'password' | 'verificationCode',
|
||||
checked: boolean
|
||||
) => void;
|
||||
onToggleVerificationPrimary: (identifier: SignInIdentifier) => void;
|
||||
onDelete: (identifier: SignInIdentifier) => void;
|
||||
onToggleVerificationPrimary: () => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
const SignInMethodItem = ({
|
||||
|
@ -32,6 +36,9 @@ const SignInMethodItem = ({
|
|||
isPasswordCheckable,
|
||||
isVerificationCodeCheckable,
|
||||
isDeletable,
|
||||
requiredConnectors,
|
||||
hasError,
|
||||
errorMessage,
|
||||
onVerificationStateChange,
|
||||
onToggleVerificationPrimary,
|
||||
onDelete,
|
||||
|
@ -39,71 +46,71 @@ const SignInMethodItem = ({
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
<div key={snakeCase(identifier)} className={styles.signInMethodItem}>
|
||||
<div className={styles.signInMethod}>
|
||||
<div className={styles.identifier}>
|
||||
<Draggable className={styles.draggableIcon} />
|
||||
{t('sign_in_exp.sign_up_and_sign_in.identifiers', {
|
||||
context: snakeCase(identifier),
|
||||
})}
|
||||
<div>
|
||||
<div key={snakeCase(identifier)} className={styles.signInMethodItem}>
|
||||
<div className={classNames(styles.signInMethod, hasError && styles.error)}>
|
||||
<div className={styles.identifier}>
|
||||
<Draggable className={styles.draggableIcon} />
|
||||
{t('sign_in_exp.sign_up_and_sign_in.identifiers', {
|
||||
context: snakeCase(identifier),
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.authentication,
|
||||
!isPasswordPrimary && styles.verifyCodePrimary
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
className={styles.checkBox}
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_in.password_auth')}
|
||||
value={password}
|
||||
disabled={!isPasswordCheckable}
|
||||
disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.password_auth')}
|
||||
onChange={(checked) => {
|
||||
onVerificationStateChange('password', checked);
|
||||
}}
|
||||
/>
|
||||
{identifier !== SignInIdentifier.Username && (
|
||||
<>
|
||||
<IconButton
|
||||
className={styles.swapButton}
|
||||
tooltip={t('sign_in_exp.sign_up_and_sign_in.sign_in.auth_swap_tip')}
|
||||
onClick={onToggleVerificationPrimary}
|
||||
>
|
||||
<SwitchArrowIcon />
|
||||
</IconButton>
|
||||
<Checkbox
|
||||
className={styles.checkBox}
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth')}
|
||||
value={verificationCode}
|
||||
disabled={!isVerificationCodeCheckable}
|
||||
disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.verification_code_auth')}
|
||||
onChange={(checked) => {
|
||||
onVerificationStateChange('verificationCode', checked);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.authentication,
|
||||
!isPasswordPrimary && styles.verifyCodePrimary
|
||||
<IconButton
|
||||
disabled={!isDeletable}
|
||||
tooltip={conditional(
|
||||
!isDeletable &&
|
||||
t('sign_in_exp.sign_up_and_sign_in.tip.delete_sign_in_method', {
|
||||
identifier: t('sign_in_exp.sign_up_and_sign_in.identifiers', {
|
||||
context: snakeCase(identifier),
|
||||
}).toLocaleLowerCase(),
|
||||
})
|
||||
)}
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Checkbox
|
||||
className={styles.checkBox}
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_in.password_auth')}
|
||||
value={password}
|
||||
disabled={!isPasswordCheckable}
|
||||
disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.password_auth')}
|
||||
onChange={(checked) => {
|
||||
onVerificationStateChange(identifier, 'password', checked);
|
||||
}}
|
||||
/>
|
||||
{identifier !== SignInIdentifier.Username && (
|
||||
<>
|
||||
<IconButton
|
||||
className={styles.swapButton}
|
||||
tooltip={t('sign_in_exp.sign_up_and_sign_in.sign_in.auth_swap_tip')}
|
||||
onClick={() => {
|
||||
onToggleVerificationPrimary(identifier);
|
||||
}}
|
||||
>
|
||||
<SwitchArrowIcon />
|
||||
</IconButton>
|
||||
<Checkbox
|
||||
className={styles.checkBox}
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth')}
|
||||
value={verificationCode}
|
||||
disabled={!isVerificationCodeCheckable}
|
||||
disabledTooltip={t('sign_in_exp.sign_up_and_sign_in.tip.verification_code_auth')}
|
||||
onChange={(checked) => {
|
||||
onVerificationStateChange(identifier, 'verificationCode', checked);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Minus />
|
||||
</IconButton>
|
||||
</div>
|
||||
<IconButton
|
||||
disabled={!isDeletable}
|
||||
tooltip={conditional(
|
||||
!isDeletable &&
|
||||
t('sign_in_exp.sign_up_and_sign_in.tip.delete_sign_in_method', {
|
||||
identifier: t('sign_in_exp.sign_up_and_sign_in.identifiers', {
|
||||
context: snakeCase(identifier),
|
||||
}).toLocaleLowerCase(),
|
||||
})
|
||||
)}
|
||||
onClick={() => {
|
||||
onDelete(identifier);
|
||||
}}
|
||||
>
|
||||
<Minus />
|
||||
</IconButton>
|
||||
{errorMessage && <div className={styles.errorMessage}>{errorMessage}</div>}
|
||||
<ConnectorSetupWarning requiredConnectors={requiredConnectors} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -22,6 +22,10 @@
|
|||
cursor: move;
|
||||
color: var(--color-text);
|
||||
|
||||
&.error {
|
||||
outline: 1px solid var(--color-error);
|
||||
}
|
||||
|
||||
.identifier {
|
||||
width: 130px;
|
||||
display: flex;
|
||||
|
@ -67,3 +71,8 @@
|
|||
.addSignInMethodDropDown {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
|
|
@ -1,178 +1,153 @@
|
|||
import type { ConnectorType } from '@logto/schemas';
|
||||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import DragDropProvider from '@/components/Transfer/DragDropProvider';
|
||||
import DraggableItem from '@/components/Transfer/DraggableItem';
|
||||
import useEnabledConnectorTypes from '@/hooks/use-enabled-connector-types';
|
||||
import type { SignInExperienceForm } from '@/pages/SignInExperience/types';
|
||||
|
||||
import { signInIdentifiers, signInIdentifierToRequiredConnectorMapping } from '../../constants';
|
||||
import ConnectorSetupWarning from '../ConnectorSetupWarning';
|
||||
import {
|
||||
signInIdentifiers,
|
||||
signInIdentifierToRequiredConnectorMapping,
|
||||
signUpIdentifierToRequiredConnectorMapping,
|
||||
signUpToSignInIdentifierMapping,
|
||||
} from '../../constants';
|
||||
import AddButton from './AddButton';
|
||||
import SignInMethodItem from './SignInMethodItem';
|
||||
import * as styles from './index.module.scss';
|
||||
import type { SignInMethod } from './types';
|
||||
import {
|
||||
computeOnSignInMethodAppended,
|
||||
computeOnVerificationStateChanged,
|
||||
computeOnPasswordPrimaryFlagToggled,
|
||||
getSignInMethodPasswordCheckState,
|
||||
getSignInMethodVerificationCodeCheckState,
|
||||
} from './utilities';
|
||||
|
||||
type Props = {
|
||||
value: SignInMethod[];
|
||||
onChange: (value: SignInMethod[]) => void;
|
||||
requiredSignInIdentifiers: SignInIdentifier[];
|
||||
ignoredWarningConnectors: ConnectorType[];
|
||||
isSignUpPasswordRequired: boolean;
|
||||
isSignUpVerificationRequired: boolean;
|
||||
};
|
||||
const SignInMethodEditBox = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
trigger,
|
||||
formState: { submitCount },
|
||||
} = useFormContext<SignInExperienceForm>();
|
||||
const signUp = watch('signUp');
|
||||
|
||||
const SignInMethodEditBox = ({
|
||||
value,
|
||||
onChange,
|
||||
requiredSignInIdentifiers,
|
||||
ignoredWarningConnectors,
|
||||
isSignUpPasswordRequired,
|
||||
isSignUpVerificationRequired,
|
||||
}: Props) => {
|
||||
const signInIdentifierOptions = signInIdentifiers.filter((candidateIdentifier) =>
|
||||
value.every(({ identifier }) => identifier !== candidateIdentifier)
|
||||
);
|
||||
const { fields, swap, update, remove, append } = useFieldArray({
|
||||
control,
|
||||
name: 'signIn.methods',
|
||||
});
|
||||
|
||||
// Note: add a reference to avoid infinite loop when change the value by `useEffect`
|
||||
const signInMethods = useRef(value);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: SignInMethod[]) => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
signInMethods.current = value;
|
||||
onChange(value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const addSignInMethod = useCallback(
|
||||
(identifier: SignInIdentifier) => {
|
||||
handleChange(
|
||||
computeOnSignInMethodAppended(value, {
|
||||
identifier,
|
||||
password: getSignInMethodPasswordCheckState(identifier, isSignUpPasswordRequired),
|
||||
verificationCode: getSignInMethodVerificationCodeCheckState(identifier),
|
||||
isPasswordPrimary: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
[handleChange, value, isSignUpPasswordRequired]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const allSignInMethods = requiredSignInIdentifiers.reduce(
|
||||
(previous, current) =>
|
||||
computeOnSignInMethodAppended(previous, {
|
||||
identifier: current,
|
||||
password: getSignInMethodPasswordCheckState(current, isSignUpPasswordRequired),
|
||||
verificationCode: getSignInMethodVerificationCodeCheckState(current),
|
||||
isPasswordPrimary: true,
|
||||
}),
|
||||
signInMethods.current
|
||||
);
|
||||
|
||||
handleChange(
|
||||
allSignInMethods.map((method) => ({
|
||||
...method,
|
||||
password: getSignInMethodPasswordCheckState(
|
||||
method.identifier,
|
||||
isSignUpPasswordRequired,
|
||||
method.password
|
||||
),
|
||||
verificationCode: getSignInMethodVerificationCodeCheckState(method.identifier),
|
||||
}))
|
||||
);
|
||||
}, [
|
||||
handleChange,
|
||||
isSignUpPasswordRequired,
|
||||
isSignUpVerificationRequired,
|
||||
requiredSignInIdentifiers,
|
||||
]);
|
||||
|
||||
const onMoveItem = (dragIndex: number, hoverIndex: number) => {
|
||||
const dragItem = value[dragIndex];
|
||||
const hoverItem = value[hoverIndex];
|
||||
|
||||
if (!dragItem || !hoverItem) {
|
||||
return;
|
||||
const revalidate = () => {
|
||||
if (submitCount) {
|
||||
void trigger(`signIn.methods`);
|
||||
}
|
||||
|
||||
handleChange(
|
||||
value.map((value_, index) => {
|
||||
if (index === dragIndex) {
|
||||
return hoverItem;
|
||||
}
|
||||
|
||||
if (index === hoverIndex) {
|
||||
return dragItem;
|
||||
}
|
||||
|
||||
return value_;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const { isConnectorTypeEnabled } = useEnabledConnectorTypes();
|
||||
|
||||
if (!signUp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
identifier: signUpIdentifier,
|
||||
password: isSignUpPasswordRequired,
|
||||
verify: isSignUpVerificationRequired,
|
||||
} = signUp;
|
||||
|
||||
const requiredSignInIdentifiers = signUpToSignInIdentifierMapping[signUpIdentifier];
|
||||
const ignoredWarningConnectors = signUpIdentifierToRequiredConnectorMapping[signUpIdentifier];
|
||||
|
||||
const signInIdentifierOptions = signInIdentifiers.filter((candidateIdentifier) =>
|
||||
fields.every(({ identifier }) => identifier !== candidateIdentifier)
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DragDropProvider>
|
||||
{value.map((signInMethod, index) => (
|
||||
<DraggableItem
|
||||
key={signInMethod.identifier}
|
||||
id={signInMethod.identifier}
|
||||
sortIndex={index}
|
||||
moveItem={onMoveItem}
|
||||
className={styles.draggleItemContainer}
|
||||
>
|
||||
<SignInMethodItem
|
||||
signInMethod={signInMethod}
|
||||
isPasswordCheckable={
|
||||
signInMethod.identifier !== SignInIdentifier.Username && !isSignUpPasswordRequired
|
||||
}
|
||||
isVerificationCodeCheckable={
|
||||
!(isSignUpVerificationRequired && !isSignUpPasswordRequired)
|
||||
}
|
||||
isDeletable={!requiredSignInIdentifiers.includes(signInMethod.identifier)}
|
||||
onVerificationStateChange={(identifier, verification, checked) => {
|
||||
handleChange(
|
||||
computeOnVerificationStateChanged(value, identifier, verification, checked)
|
||||
);
|
||||
}}
|
||||
onToggleVerificationPrimary={(identifier) => {
|
||||
handleChange(computeOnPasswordPrimaryFlagToggled(value, identifier));
|
||||
}}
|
||||
onDelete={(identifier) => {
|
||||
handleChange(value.filter((method) => method.identifier !== identifier));
|
||||
}}
|
||||
/>
|
||||
</DraggableItem>
|
||||
))}
|
||||
{fields.map((signInMethod, index) => {
|
||||
const { id, identifier, verificationCode, isPasswordPrimary } = signInMethod;
|
||||
|
||||
const requiredConnectors =
|
||||
conditional(
|
||||
verificationCode &&
|
||||
signInIdentifierToRequiredConnectorMapping[identifier].filter(
|
||||
(connector) => !ignoredWarningConnectors.includes(connector)
|
||||
)
|
||||
) ?? [];
|
||||
|
||||
return (
|
||||
<DraggableItem
|
||||
key={id}
|
||||
id={id}
|
||||
sortIndex={index}
|
||||
moveItem={swap}
|
||||
className={styles.draggleItemContainer}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`signIn.methods.${index}`}
|
||||
rules={{
|
||||
validate: ({ password, verificationCode }) => {
|
||||
if (!password && !verificationCode) {
|
||||
return t('sign_in_exp.sign_up_and_sign_in.sign_in.require_auth_factor');
|
||||
}
|
||||
|
||||
if (
|
||||
verificationCode &&
|
||||
requiredConnectors.some(
|
||||
(connectorType) => !isConnectorTypeEnabled(connectorType)
|
||||
)
|
||||
) {
|
||||
// Note: when required connectors are not all enabled, we show error state without error message for we have the connector setup warning
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}}
|
||||
render={({ field: { value }, fieldState: { error } }) => (
|
||||
<SignInMethodItem
|
||||
signInMethod={value}
|
||||
isPasswordCheckable={
|
||||
identifier !== SignInIdentifier.Username && !isSignUpPasswordRequired
|
||||
}
|
||||
isVerificationCodeCheckable={
|
||||
!(isSignUpVerificationRequired && !isSignUpPasswordRequired)
|
||||
}
|
||||
isDeletable={!requiredSignInIdentifiers.includes(identifier)}
|
||||
requiredConnectors={requiredConnectors}
|
||||
hasError={Boolean(error)}
|
||||
errorMessage={error?.message}
|
||||
onVerificationStateChange={(verification, checked) => {
|
||||
update(index, { ...value, [verification]: checked });
|
||||
revalidate();
|
||||
}}
|
||||
onToggleVerificationPrimary={() => {
|
||||
update(index, { ...value, isPasswordPrimary: !isPasswordPrimary });
|
||||
revalidate();
|
||||
}}
|
||||
onDelete={() => {
|
||||
remove(index);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</DraggableItem>
|
||||
);
|
||||
})}
|
||||
</DragDropProvider>
|
||||
<ConnectorSetupWarning
|
||||
requiredConnectors={value
|
||||
.reduce<ConnectorType[]>(
|
||||
(connectors, { identifier: signInIdentifier, verificationCode }) => {
|
||||
return [
|
||||
...connectors,
|
||||
...(verificationCode
|
||||
? signInIdentifierToRequiredConnectorMapping[signInIdentifier]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
[]
|
||||
)
|
||||
.filter((connector) => !ignoredWarningConnectors.includes(connector))}
|
||||
/>
|
||||
<AddButton
|
||||
options={signInIdentifierOptions}
|
||||
hasSelectedIdentifiers={value.length > 0}
|
||||
onSelected={addSignInMethod}
|
||||
hasSelectedIdentifiers={fields.length > 0}
|
||||
onSelected={(identifier) => {
|
||||
append({
|
||||
identifier,
|
||||
password: getSignInMethodPasswordCheckState(identifier, isSignUpPasswordRequired),
|
||||
verificationCode: getSignInMethodVerificationCodeCheckState(identifier),
|
||||
isPasswordPrimary: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,48 +1,5 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
|
||||
import type { SignInMethod } from './types';
|
||||
|
||||
export const computeOnVerificationStateChanged = (
|
||||
oldValue: SignInMethod[],
|
||||
identifier: SignInIdentifier,
|
||||
verification: 'password' | 'verificationCode',
|
||||
checked: boolean
|
||||
) =>
|
||||
oldValue.map((method) =>
|
||||
method.identifier === identifier
|
||||
? {
|
||||
...method,
|
||||
[verification]: checked,
|
||||
}
|
||||
: method
|
||||
);
|
||||
|
||||
export const computeOnSignInMethodAppended = (
|
||||
appendTo: SignInMethod[],
|
||||
appended: SignInMethod
|
||||
): SignInMethod[] => {
|
||||
const { identifier: signInIdentifier } = appended;
|
||||
|
||||
if (appendTo.some((method) => method.identifier === signInIdentifier)) {
|
||||
return appendTo;
|
||||
}
|
||||
|
||||
return [...appendTo, appended];
|
||||
};
|
||||
|
||||
export const computeOnPasswordPrimaryFlagToggled = (
|
||||
oldValue: SignInMethod[],
|
||||
identifier: SignInIdentifier
|
||||
) =>
|
||||
oldValue.map((method) =>
|
||||
method.identifier === identifier
|
||||
? {
|
||||
...method,
|
||||
isPasswordPrimary: !method.isPasswordPrimary,
|
||||
}
|
||||
: method
|
||||
);
|
||||
|
||||
export const getSignInMethodPasswordCheckState = (
|
||||
signInIdentifier: SignInIdentifier,
|
||||
isSignUpPasswordRequired: boolean,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { SignInExperience, SignUp } from '@logto/schemas';
|
||||
|
||||
export type SignInExperienceForm = Omit<SignInExperience, 'signInMethods' | 'signUp'> & {
|
||||
signUp: Partial<SignUp>;
|
||||
signUp?: SignUp;
|
||||
createAccountEnabled: boolean;
|
||||
};
|
||||
|
|
|
@ -30,10 +30,10 @@ export const signInExperienceParser = {
|
|||
darkLogoUrl: conditional(branding.darkLogoUrl?.length && branding.darkLogoUrl),
|
||||
slogan: conditional(branding.slogan?.length && branding.slogan),
|
||||
},
|
||||
signUp: {
|
||||
identifier: signUp.identifier ?? SignUpIdentifier.Username,
|
||||
password: Boolean(signUp.password),
|
||||
verify: Boolean(signUp.verify),
|
||||
signUp: signUp ?? {
|
||||
identifier: SignUpIdentifier.Username,
|
||||
password: true,
|
||||
verify: false,
|
||||
},
|
||||
signInMode: createAccountEnabled ? SignInMode.SignInAndRegister : SignInMode.SignIn,
|
||||
};
|
||||
|
|
|
@ -46,6 +46,7 @@ const sign_in_exp = {
|
|||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
require_auth_factor: 'You have to select at least one authentication factor.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
|
|
|
@ -68,6 +68,7 @@ const sign_in_exp = {
|
|||
password_auth: 'Password',
|
||||
verification_code_auth: 'Verification code',
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.',
|
||||
require_auth_factor: 'You have to select at least one authentication factor.',
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN-IN',
|
||||
|
|
|
@ -70,6 +70,7 @@ const sign_in_exp = {
|
|||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
require_auth_factor: 'You have to select at least one authentication factor.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
|
|
|
@ -65,6 +65,7 @@ const sign_in_exp = {
|
|||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
require_auth_factor: 'You have to select at least one authentication factor.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
|
|
|
@ -68,6 +68,7 @@ const sign_in_exp = {
|
|||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
require_auth_factor: 'You have to select at least one authentication factor.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
|
|
|
@ -69,6 +69,7 @@ const sign_in_exp = {
|
|||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap the options below to determine which appears first in the flow.', // UNTRANSLATED
|
||||
require_auth_factor: 'You have to select at least one authentication factor.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: 'SOCIAL SIGN-IN', // UNTRANSLATED
|
||||
|
|
|
@ -63,6 +63,7 @@ const sign_in_exp = {
|
|||
password_auth: '密码',
|
||||
verification_code_auth: '验证码',
|
||||
auth_swap_tip: '交换以下选项的位置即可设定它们在用户登录流程中出现的先后。',
|
||||
require_auth_factor: 'You have to select at least one authentication factor.', // UNTRANSLATED
|
||||
},
|
||||
social_sign_in: {
|
||||
title: '社交登录',
|
||||
|
|
Loading…
Add table
Reference in a new issue