mirror of
https://github.com/logto-io/logto.git
synced 2025-02-03 21:48:55 -05:00
feat(console): sie sign-in form (#2229)
This commit is contained in:
parent
569069524e
commit
75b6d3946a
20 changed files with 622 additions and 97 deletions
3
packages/console/src/assets/images/switch-arrow.svg
Normal file
3
packages/console/src/assets/images/switch-arrow.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.5915 7.7415L15.2581 4.40816C15.1804 4.33046 15.0882 4.26883 14.9867 4.22678C14.8851 4.18473 14.7763 4.16309 14.6665 4.16309C14.4445 4.16309 14.2317 4.25124 14.0748 4.40816C13.9179 4.56508 13.8297 4.77791 13.8297 4.99983C13.8297 5.22175 13.9179 5.43458 14.0748 5.5915L15.9915 7.49983H6.33312C6.11211 7.49983 5.90015 7.58763 5.74387 7.74391C5.58759 7.90019 5.49979 8.11215 5.49979 8.33316C5.49979 8.55418 5.58759 8.76614 5.74387 8.92242C5.90015 9.0787 6.11211 9.1665 6.33312 9.1665H17.9998C18.1643 9.16567 18.3249 9.11617 18.4613 9.02423C18.5978 8.93228 18.7039 8.80201 18.7665 8.64983C18.8303 8.49807 18.8477 8.33081 18.8166 8.16915C18.7854 8.00749 18.7071 7.85868 18.5915 7.7415ZM14.6665 10.8332H2.99979C2.83527 10.834 2.67467 10.8835 2.53824 10.9754C2.40181 11.0674 2.29564 11.1976 2.23312 11.3498C2.16931 11.5016 2.15187 11.6688 2.18302 11.8305C2.21416 11.9922 2.29249 12.141 2.40812 12.2582L5.74146 15.5915C5.81893 15.6696 5.91109 15.7316 6.01264 15.7739C6.11419 15.8162 6.22311 15.838 6.33312 15.838C6.44313 15.838 6.55206 15.8162 6.6536 15.7739C6.75515 15.7316 6.84732 15.6696 6.92479 15.5915C7.0029 15.514 7.06489 15.4219 7.1072 15.3203C7.14951 15.2188 7.17129 15.1098 7.17129 14.9998C7.17129 14.8898 7.14951 14.7809 7.1072 14.6793C7.06489 14.5778 7.0029 14.4856 6.92479 14.4082L5.00812 12.4998H14.6665C14.8875 12.4998 15.0994 12.412 15.2557 12.2558C15.412 12.0995 15.4998 11.8875 15.4998 11.6665C15.4998 11.4455 15.412 11.2335 15.2557 11.0772C15.0994 10.921 14.8875 10.8332 14.6665 10.8332Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -1,61 +0,0 @@
|
|||
import type { ConnectorResponse } from '@logto/schemas';
|
||||
import { ConnectorType, SignUpIdentifier } from '@logto/schemas';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { snakeCase } from 'snake-case';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
|
||||
type Props = {
|
||||
signUpIdentifier: SignUpIdentifier;
|
||||
};
|
||||
|
||||
const ConnectorSetupWarning = ({ signUpIdentifier }: Props) => {
|
||||
const { data: connectors } = useSWR<ConnectorResponse[], RequestError>('/api/connectors');
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const connectorTypes = useMemo(() => {
|
||||
if (signUpIdentifier === SignUpIdentifier.Username) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (signUpIdentifier === SignUpIdentifier.Email) {
|
||||
return [ConnectorType.Email];
|
||||
}
|
||||
|
||||
if (signUpIdentifier === SignUpIdentifier.Phone) {
|
||||
return [ConnectorType.Sms];
|
||||
}
|
||||
|
||||
if (signUpIdentifier === SignUpIdentifier.EmailOrPhone) {
|
||||
return [ConnectorType.Email, ConnectorType.Sms];
|
||||
}
|
||||
|
||||
return [ConnectorType.Social];
|
||||
}, [signUpIdentifier]);
|
||||
|
||||
if (connectorTypes.length === 0 || !connectors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
connectorTypes.every((connectorType) =>
|
||||
connectors.some(({ type, enabled }) => type === connectorType && enabled)
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert
|
||||
action="general.set_up"
|
||||
href={connectorTypes.includes(ConnectorType.Social) ? '/connectors/social' : '/connectors'}
|
||||
>
|
||||
{t('sign_in_exp.setup_warning.no_connector', { context: snakeCase(signUpIdentifier) })}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectorSetupWarning;
|
|
@ -0,0 +1,46 @@
|
|||
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 { 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 requirePassword = watch('signUp.password');
|
||||
const requireVerificationCode = watch('signUp.verify');
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.title}>{t('sign_in_exp.sign_up_and_sign_in.sign_in.title')}</div>
|
||||
<FormField title="sign_in_exp.sign_up_and_sign_in.sign_in.sign_in_identifier_and_auth">
|
||||
<div className={styles.signInDescription}>
|
||||
{t('sign_in_exp.sign_up_and_sign_in.sign_in.description')}
|
||||
</div>
|
||||
<Controller
|
||||
control={control}
|
||||
name="signIn.methods"
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return (
|
||||
<SignInMethodEditBox
|
||||
value={value}
|
||||
requiredSignInIdentifiers={signUpToSignInIdentifierMapping[signUpIdentifier]}
|
||||
isPasswordRequired={requirePassword}
|
||||
isVerificationRequired={requireVerificationCode}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignInForm;
|
|
@ -9,17 +9,14 @@ import FormField from '@/components/FormField';
|
|||
import Select from '@/components/Select';
|
||||
|
||||
import type { SignInExperienceForm } from '../../types';
|
||||
import ConnectorSetupWarning from './ConnectorSetupWarning';
|
||||
import ConnectorSetupWarning from './components/ConnectorSetupWarning';
|
||||
import {
|
||||
requiredVerifySignUpIdentifiers,
|
||||
signUpIdentifiers,
|
||||
signUpIdentifierToRequiredConnectorMapping,
|
||||
} from './constants';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const signUpIdentifiers = Object.values(SignUpIdentifier);
|
||||
|
||||
const requireVerifyIdentifiers = new Set([
|
||||
SignUpIdentifier.Email,
|
||||
SignUpIdentifier.Phone,
|
||||
SignUpIdentifier.EmailOrPhone,
|
||||
]);
|
||||
|
||||
const SignUpForm = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { control, setValue, resetField, watch } = useFormContext<SignInExperienceForm>();
|
||||
|
@ -40,7 +37,7 @@ const SignUpForm = () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (requireVerifyIdentifiers.has(signUpIdentifier)) {
|
||||
if (requiredVerifySignUpIdentifiers.includes(signUpIdentifier)) {
|
||||
resetField('signUp.password');
|
||||
setValue('signUp.verify', true);
|
||||
}
|
||||
|
@ -80,7 +77,9 @@ const SignUpForm = () => {
|
|||
)}
|
||||
/>
|
||||
{signUpIdentifier !== SignUpIdentifier.None && (
|
||||
<ConnectorSetupWarning signUpIdentifier={signUpIdentifier} />
|
||||
<ConnectorSetupWarning
|
||||
requiredConnectors={signUpIdentifierToRequiredConnectorMapping[signUpIdentifier]}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
{signUpIdentifier !== SignUpIdentifier.None && (
|
||||
|
@ -108,7 +107,7 @@ const SignUpForm = () => {
|
|||
<Checkbox
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_up.verify_at_sign_up_option')}
|
||||
value={value}
|
||||
disabled={requireVerifyIdentifiers.has(signUpIdentifier)}
|
||||
disabled={requiredVerifySignUpIdentifiers.includes(signUpIdentifier)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
import type { ConnectorResponse } from '@logto/schemas';
|
||||
import { ConnectorType } from '@logto/schemas';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import Alert from '@/components/Alert';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
|
||||
type Props = {
|
||||
requiredConnectors: ConnectorType[];
|
||||
};
|
||||
|
||||
const ConnectorSetupWarning = ({ requiredConnectors }: Props) => {
|
||||
const { data: connectors } = useSWR<ConnectorResponse[], RequestError>('/api/connectors');
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
if (!connectors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const missingConnectors = (
|
||||
requiredConnectors.length === 0 ? [ConnectorType.Social] : requiredConnectors
|
||||
).filter(
|
||||
(connectorType) => !connectors.some(({ type, enabled }) => type === connectorType && enabled)
|
||||
);
|
||||
|
||||
if (missingConnectors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{missingConnectors.map((connectorType) => (
|
||||
<Alert
|
||||
key={connectorType}
|
||||
action="general.set_up"
|
||||
href={connectorType === ConnectorType.Social ? '/connectors/social' : '/connectors'}
|
||||
>
|
||||
{t('sign_in_exp.setup_warning.no_connector', {
|
||||
context: connectorType.toLowerCase(),
|
||||
})}
|
||||
</Alert>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectorSetupWarning;
|
|
@ -0,0 +1,64 @@
|
|||
import type { SignInIdentifier } from '@logto/schemas';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Dropdown, { DropdownItem } from '@/components/Dropdown';
|
||||
|
||||
type Props = {
|
||||
options: SignInIdentifier[];
|
||||
onSelected: (signInIdentifier: SignInIdentifier) => void;
|
||||
};
|
||||
|
||||
// TODO: @yijun extract this component to share with the future add social button
|
||||
const AddSignInMethodButton = ({ options, onSelected }: Props) => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const anchorRef = useRef<HTMLDivElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
if (options.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidates = options.map((identifier) => ({
|
||||
value: identifier,
|
||||
title: t('sign_in_exp.sign_up_and_sign_in.identifiers', { context: snakeCase(identifier) }),
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={anchorRef}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
title="general.add_another"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Dropdown
|
||||
anchorRef={anchorRef}
|
||||
isOpen={isOpen}
|
||||
horizontalAlign="start"
|
||||
onClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{candidates.map(({ value, title }) => (
|
||||
<DropdownItem
|
||||
key={value}
|
||||
onClick={() => {
|
||||
onSelected(value);
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddSignInMethodButton;
|
|
@ -0,0 +1,97 @@
|
|||
import { SignInIdentifier } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { snakeCase } from 'snake-case';
|
||||
|
||||
import Draggable from '@/assets/images/draggable.svg';
|
||||
import Minus from '@/assets/images/minus.svg';
|
||||
import SwitchArrowIcon from '@/assets/images/switch-arrow.svg';
|
||||
import Checkbox from '@/components/Checkbox';
|
||||
import IconButton from '@/components/IconButton';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
import type { SignInMethod } from './types';
|
||||
|
||||
type Props = {
|
||||
signInMethod: SignInMethod;
|
||||
isPasswordRequired: boolean;
|
||||
isVerificationRequired: boolean;
|
||||
isDeletable: boolean;
|
||||
onVerificationStateChange: (
|
||||
identifier: SignInIdentifier,
|
||||
verification: 'password' | 'verificationCode',
|
||||
checked: boolean
|
||||
) => void;
|
||||
onToggleVerificationPrimary: (identifier: SignInIdentifier) => void;
|
||||
onDelete: (identifier: SignInIdentifier) => void;
|
||||
};
|
||||
|
||||
const SignInMethodItem = ({
|
||||
signInMethod: { identifier, password, verificationCode, isPasswordPrimary },
|
||||
isPasswordRequired,
|
||||
isVerificationRequired,
|
||||
isDeletable,
|
||||
onVerificationStateChange,
|
||||
onToggleVerificationPrimary,
|
||||
onDelete,
|
||||
}: Props) => {
|
||||
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
|
||||
className={classNames(
|
||||
styles.authentication,
|
||||
!isPasswordPrimary && styles.verifyCodePrimary
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_in.password_auth')}
|
||||
value={password}
|
||||
disabled={isPasswordRequired}
|
||||
onChange={(checked) => {
|
||||
onVerificationStateChange(identifier, 'password', checked);
|
||||
}}
|
||||
/>
|
||||
{identifier !== SignInIdentifier.Username && (
|
||||
<>
|
||||
<IconButton
|
||||
tooltip="sign_in_exp.sign_up_and_sign_in.sign_in.auth_swap_tip"
|
||||
onClick={() => {
|
||||
onToggleVerificationPrimary(identifier);
|
||||
}}
|
||||
>
|
||||
<SwitchArrowIcon />
|
||||
</IconButton>
|
||||
<Checkbox
|
||||
label={t('sign_in_exp.sign_up_and_sign_in.sign_in.verification_code_auth')}
|
||||
value={verificationCode}
|
||||
disabled={isVerificationRequired && !isPasswordRequired}
|
||||
onChange={(checked) => {
|
||||
onVerificationStateChange(identifier, 'verificationCode', checked);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<IconButton
|
||||
disabled={isDeletable}
|
||||
onClick={() => {
|
||||
onDelete(identifier);
|
||||
}}
|
||||
>
|
||||
<Minus />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignInMethodItem;
|
|
@ -0,0 +1,43 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.signInMethodItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: _.unit(2) 0;
|
||||
}
|
||||
|
||||
.signInMethod {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
margin-right: _.unit(2);
|
||||
padding: _.unit(3) _.unit(2);
|
||||
background-color: var(--color-layer-2);
|
||||
border-radius: 8px;
|
||||
cursor: move;
|
||||
color: var(--color-text);
|
||||
|
||||
.identifier {
|
||||
width: 130px;
|
||||
display: flex;
|
||||
font: var(--font-label-large);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.authentication {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 _.unit(2);
|
||||
flex-grow: 1;
|
||||
|
||||
&.verifyCodePrimary {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.draggableIcon {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
import type { ConnectorType, SignInIdentifier } from '@logto/schemas';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import DragDropProvider from '@/components/Transfer/DragDropProvider';
|
||||
import DraggableItem from '@/components/Transfer/DraggableItem';
|
||||
|
||||
import { signInIdentifiers, signInIdentifierToRequiredConnectorMapping } from '../../constants';
|
||||
import ConnectorSetupWarning from '../ConnectorSetupWarning';
|
||||
import AddSignInMethodButton from './AddSignInMethodButton';
|
||||
import SignInMethodItem from './SignInMethodItem';
|
||||
import type { SignInMethod } from './types';
|
||||
import {
|
||||
computeOnSignInMethodAppended as appendSignInMethodIfNotExist,
|
||||
computeOnVerificationStateChanged,
|
||||
computeOnPasswordPrimaryFlagToggled,
|
||||
} from './utilities';
|
||||
|
||||
type Props = {
|
||||
value: SignInMethod[];
|
||||
onChange: (value: SignInMethod[]) => void;
|
||||
requiredSignInIdentifiers: SignInIdentifier[];
|
||||
isPasswordRequired: boolean;
|
||||
isVerificationRequired: boolean;
|
||||
};
|
||||
|
||||
const SignInMethodEditBox = ({
|
||||
value,
|
||||
onChange,
|
||||
requiredSignInIdentifiers,
|
||||
isPasswordRequired,
|
||||
isVerificationRequired,
|
||||
}: Props) => {
|
||||
const signInIdentifierOptions = signInIdentifiers.filter((candidateIdentifier) =>
|
||||
value.every(({ identifier }) => identifier !== candidateIdentifier)
|
||||
);
|
||||
|
||||
// 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(
|
||||
appendSignInMethodIfNotExist(value, identifier, isPasswordRequired, isVerificationRequired)
|
||||
);
|
||||
},
|
||||
[handleChange, value, isPasswordRequired, isVerificationRequired]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const requiredSignInMethods = requiredSignInIdentifiers.reduce(
|
||||
(previous, current) =>
|
||||
appendSignInMethodIfNotExist(previous, current, isPasswordRequired, isVerificationRequired),
|
||||
signInMethods.current
|
||||
);
|
||||
|
||||
handleChange(
|
||||
requiredSignInMethods.map((method) => ({
|
||||
...method,
|
||||
password: isPasswordRequired,
|
||||
verificationCode: isVerificationRequired,
|
||||
}))
|
||||
);
|
||||
}, [handleChange, isPasswordRequired, isVerificationRequired, requiredSignInIdentifiers]);
|
||||
|
||||
const onMoveItem = (dragIndex: number, hoverIndex: number) => {
|
||||
const dragItem = value[dragIndex];
|
||||
const hoverItem = value[hoverIndex];
|
||||
|
||||
if (!dragItem || !hoverItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleChange(
|
||||
value.map((value_, index) => {
|
||||
if (index === dragIndex) {
|
||||
return hoverItem;
|
||||
}
|
||||
|
||||
if (index === hoverIndex) {
|
||||
return dragItem;
|
||||
}
|
||||
|
||||
return value_;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DragDropProvider>
|
||||
{value.map((signInMethod, index) => (
|
||||
<DraggableItem
|
||||
key={signInMethod.identifier}
|
||||
id={signInMethod.identifier}
|
||||
sortIndex={index}
|
||||
moveItem={onMoveItem}
|
||||
>
|
||||
<SignInMethodItem
|
||||
signInMethod={signInMethod}
|
||||
isPasswordRequired={isPasswordRequired}
|
||||
isVerificationRequired={isVerificationRequired}
|
||||
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>
|
||||
))}
|
||||
</DragDropProvider>
|
||||
{requiredSignInIdentifiers.length > 0 && (
|
||||
<ConnectorSetupWarning
|
||||
requiredConnectors={requiredSignInIdentifiers.reduce<ConnectorType[]>(
|
||||
(connectors, signInIdentifier) => {
|
||||
return [
|
||||
...connectors,
|
||||
...signInIdentifierToRequiredConnectorMapping[signInIdentifier],
|
||||
];
|
||||
},
|
||||
[]
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<AddSignInMethodButton options={signInIdentifierOptions} onSelected={addSignInMethod} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignInMethodEditBox;
|
|
@ -0,0 +1,3 @@
|
|||
import type { SignInExperience } from '@logto/schemas';
|
||||
|
||||
export type SignInMethod = SignInExperience['signIn']['methods'][number];
|
|
@ -0,0 +1,52 @@
|
|||
import type { 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 = (
|
||||
oldValue: SignInMethod[],
|
||||
signInIdentifier: SignInIdentifier,
|
||||
requirePassword: boolean,
|
||||
requireVerificationCode: boolean
|
||||
) => {
|
||||
if (oldValue.some((method) => method.identifier === signInIdentifier)) {
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
return [
|
||||
...oldValue,
|
||||
{
|
||||
identifier: signInIdentifier,
|
||||
password: requirePassword,
|
||||
verificationCode: requireVerificationCode,
|
||||
isPasswordPrimary: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const computeOnPasswordPrimaryFlagToggled = (
|
||||
oldValue: SignInMethod[],
|
||||
identifier: SignInIdentifier
|
||||
) =>
|
||||
oldValue.map((method) =>
|
||||
method.identifier === identifier
|
||||
? {
|
||||
...method,
|
||||
isPasswordPrimary: !method.isPasswordPrimary,
|
||||
}
|
||||
: method
|
||||
);
|
|
@ -0,0 +1,37 @@
|
|||
import { SignUpIdentifier, SignInIdentifier, ConnectorType } from '@logto/schemas';
|
||||
|
||||
export const signUpIdentifiers = Object.values(SignUpIdentifier);
|
||||
|
||||
export const signInIdentifiers = Object.values(SignInIdentifier);
|
||||
|
||||
export const requiredVerifySignUpIdentifiers = [
|
||||
SignUpIdentifier.Email,
|
||||
SignUpIdentifier.Phone,
|
||||
SignUpIdentifier.EmailOrPhone,
|
||||
];
|
||||
|
||||
export const signUpToSignInIdentifierMapping: { [key in SignUpIdentifier]: SignInIdentifier[] } = {
|
||||
[SignUpIdentifier.Username]: [SignInIdentifier.Username],
|
||||
[SignUpIdentifier.Email]: [SignInIdentifier.Email],
|
||||
[SignUpIdentifier.Phone]: [SignInIdentifier.Phone],
|
||||
[SignUpIdentifier.EmailOrPhone]: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
||||
[SignUpIdentifier.None]: [],
|
||||
};
|
||||
|
||||
export const signUpIdentifierToRequiredConnectorMapping: {
|
||||
[key in SignUpIdentifier]: ConnectorType[];
|
||||
} = {
|
||||
[SignUpIdentifier.Username]: [],
|
||||
[SignUpIdentifier.Email]: [ConnectorType.Email],
|
||||
[SignUpIdentifier.Phone]: [ConnectorType.Sms],
|
||||
[SignUpIdentifier.EmailOrPhone]: [ConnectorType.Email, ConnectorType.Sms],
|
||||
[SignUpIdentifier.None]: [],
|
||||
};
|
||||
|
||||
export const signInIdentifierToRequiredConnectorMapping: {
|
||||
[key in SignInIdentifier]: ConnectorType[];
|
||||
} = {
|
||||
[SignInIdentifier.Username]: [],
|
||||
[SignInIdentifier.Email]: [ConnectorType.Email],
|
||||
[SignInIdentifier.Phone]: [ConnectorType.Sms],
|
||||
};
|
|
@ -20,3 +20,8 @@
|
|||
margin-top: _.unit(3);
|
||||
}
|
||||
}
|
||||
|
||||
.signInDescription {
|
||||
font: var(--font-body-medium);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useFormContext } from 'react-hook-form';
|
|||
import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
|
||||
|
||||
import type { SignInExperienceForm } from '../../types';
|
||||
import SignInForm from './SignInForm';
|
||||
import SignUpForm from './SignUpForm';
|
||||
|
||||
type Props = {
|
||||
|
@ -23,6 +24,7 @@ const SignUpAndSignInTab = ({ defaultData, isDataDirty }: Props) => {
|
|||
return (
|
||||
<>
|
||||
<SignUpForm />
|
||||
<SignInForm />
|
||||
<UnsavedChangesAlertModal hasUnsavedChanges={isDataDirty} />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -53,6 +53,15 @@ const sign_in_exp = {
|
|||
verify_at_sign_up_option: 'Verify at sign up',
|
||||
social_only_creation_description: '(This apply to social only account creation)',
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN',
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication',
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.',
|
||||
password_auth: 'Password',
|
||||
verification_code_auth: 'Verification code',
|
||||
auth_swap_tip: 'Swap to change the priority',
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'SIGN-IN METHODS',
|
||||
|
@ -126,13 +135,11 @@ const sign_in_exp = {
|
|||
},
|
||||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_phone:
|
||||
no_connector_sms:
|
||||
'You haven’t set up a SMS connector yet. Your sign in experience won’t go live until you finish the settings first. ',
|
||||
no_connector_email:
|
||||
'You haven’t set up an Email connector yet. Your sign in experience won’t go live until you finish the settings first. ',
|
||||
no_connector_email_or_phone:
|
||||
'You haven’t set up both Email and SMS connectors yet. Your sign in experience won’t go live until you finish the settings first. ',
|
||||
no_connector_none:
|
||||
no_connector_social:
|
||||
'You haven’t set up any social connectors yet. Your sign in experience won’t go live until you finish the settings first. ',
|
||||
no_added_social_connector:
|
||||
'You’ve set up a few social connectors now. Make sure to add some to your sign in experience.',
|
||||
|
|
|
@ -55,6 +55,15 @@ const sign_in_exp = {
|
|||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'METHODES DE CONNEXION',
|
||||
|
@ -128,13 +137,11 @@ const sign_in_exp = {
|
|||
},
|
||||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_phone:
|
||||
no_connector_sms:
|
||||
"Vous n'avez pas encore configuré de connecteur SMS. Votre expérience de connexion ne sera pas disponible tant que vous n'aurez pas terminé les paramètres. ",
|
||||
no_connector_email:
|
||||
"Vous n'avez pas encore configuré de connecteur Email. Votre expérience de connexion ne sera pas disponible tant que vous n'aurez pas terminé les paramètres. ",
|
||||
no_connector_email_or_phone:
|
||||
'You haven’t set up both Email and SMS connectors yet. Your sign in experience won’t go live until you finish the settings first. ', // UNTRANSLATED
|
||||
no_connector_none:
|
||||
no_connector_social:
|
||||
"Vous n'avez pas encore configuré de connecteurs sociaux. Votre expérience de connexion ne sera pas disponible tant que vous n'aurez pas terminé les paramètres. ",
|
||||
no_added_social_connector:
|
||||
"Vous avez maintenant configuré quelques connecteurs sociaux. Assurez-vous d'en ajouter quelques-uns à votre expérience de connexion.",
|
||||
|
|
|
@ -50,6 +50,15 @@ const sign_in_exp = {
|
|||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: '로그인 방법',
|
||||
|
@ -123,13 +132,11 @@ const sign_in_exp = {
|
|||
},
|
||||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_phone:
|
||||
no_connector_sms:
|
||||
'SMS 연동이 아직 설정되지 않았어요. 설정이 완료될 때 까지, 사용자는 이 로그인 방법을 사용할 수 없어요.',
|
||||
no_connector_email:
|
||||
'이메일 연동이 아직 설정되지 않았어요. 설정이 완료될 때 까지, 사용자는 이 로그인 방법을 사용할 수 없어요.',
|
||||
no_connector_email_or_phone:
|
||||
'You haven’t set up both Email and SMS connectors yet. Your sign in experience won’t go live until you finish the settings first. ', // UNTRANSLATED
|
||||
no_connector_none:
|
||||
no_connector_social:
|
||||
'소셜 연동이 아직 설정되지 않았어요. 설정이 완료될 때 까지, 사용자는 이 로그인 방법을 사용할 수 없어요.',
|
||||
no_added_social_connector:
|
||||
'보다 많은 소셜 연동들을 설정하여, 고객에게 보다 나은 경험을 제공해보세요.',
|
||||
|
|
|
@ -53,6 +53,15 @@ const sign_in_exp = {
|
|||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'MÉTODOS DE LOGIN',
|
||||
|
@ -126,13 +135,11 @@ const sign_in_exp = {
|
|||
},
|
||||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_phone:
|
||||
no_connector_sms:
|
||||
'Ainda não configurou um conector de SMS. A experiência de login não será ativada até que conclua as configurações primeiro. ',
|
||||
no_connector_email:
|
||||
'Ainda não configurou um conector de email. A experiência de login não será ativada até que conclua as configurações primeiro. ',
|
||||
no_connector_email_or_phone:
|
||||
'You haven’t set up both Email and SMS connectors yet. Your sign in experience won’t go live until you finish the settings first. ', // UNTRANSLATED
|
||||
no_connector_none:
|
||||
no_connector_social:
|
||||
'Ainda não configurou um conector social. A experiência de login não será ativada até que conclua as configurações primeiro. ',
|
||||
no_added_social_connector:
|
||||
'Configurou alguns conectores sociais agora. Certifique-se de adicionar alguns a experiência de login.',
|
||||
|
|
|
@ -54,6 +54,15 @@ const sign_in_exp = {
|
|||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'OTURUM AÇMA YÖNTEMLERİ',
|
||||
|
@ -127,13 +136,11 @@ const sign_in_exp = {
|
|||
},
|
||||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_phone:
|
||||
no_connector_sms:
|
||||
'Henüz bir SMS bağlayıcısı kurmadınız. Öncelikle ayarları tamamlayana kadar oturum açma deneyiminiz yayınlanmayacaktır. ',
|
||||
no_connector_email:
|
||||
'Henüz bir e-posta adresi bağlayıcısı kurmadınız. Öncelikle ayarları tamamlayana kadar oturum açma deneyiminiz yayınlanmayacaktır. ',
|
||||
no_connector_email_or_phone:
|
||||
'You haven’t set up both Email and SMS connectors yet. Your sign in experience won’t go live until you finish the settings first. ', // UNTRANSLATED
|
||||
no_connector_none:
|
||||
no_connector_social:
|
||||
'Henüz herhangi bir social connector kurmadınız. Öncelikle ayarları tamamlayana kadar oturum açma deneyiminiz yayınlanmayacaktır. ',
|
||||
no_added_social_connector:
|
||||
'Şimdi birkaç social connector kurdunuz. Oturum açma deneyiminize bazı şeyler eklediğinizden emin olun.',
|
||||
|
|
|
@ -51,6 +51,15 @@ const sign_in_exp = {
|
|||
verify_at_sign_up_option: 'Verify at sign up', // UNTRANSLATED
|
||||
social_only_creation_description: '(This apply to social only account creation)', // UNTRANSLATED
|
||||
},
|
||||
sign_in: {
|
||||
title: 'SIGN IN', // UNTRANSLATED
|
||||
sign_in_identifier_and_auth: 'Sign in identifier and authentication', // UNTRANSLATED
|
||||
description:
|
||||
'Users can use any one of the selected ways to sign in. Drag and drop to define identifier priority regarding the sign in flow. You can also define the password or verification code priority.', // UNTRANSLATED
|
||||
password_auth: 'Password', // UNTRANSLATED
|
||||
verification_code_auth: 'Verification code', // UNTRANSLATED
|
||||
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
|
||||
},
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: '登录方式',
|
||||
|
@ -121,11 +130,9 @@ const sign_in_exp = {
|
|||
},
|
||||
setup_warning: {
|
||||
no_connector: '',
|
||||
no_connector_phone: '你还没有设置 SMS 连接器。你需完成设置后登录体验才会生效。',
|
||||
no_connector_sms: '你还没有设置 SMS 连接器。你需完成设置后登录体验才会生效。',
|
||||
no_connector_email: '你还没有设置 email 连接器。你需完成设置后登录体验才会生效。',
|
||||
no_connector_email_or_phone:
|
||||
'你还没有设置 email 和 SMS 连接器。你需完成设置后登录体验才会生效。',
|
||||
no_connector_none: '你还没有设置社交连接器。你需完成设置后登录体验才会生效。',
|
||||
no_connector_social: '你还没有设置社交连接器。你需完成设置后登录体验才会生效。',
|
||||
no_added_social_connector: '你已经成功设置了一些社交连接器。点按「+」添加一些到你的登录体验。',
|
||||
},
|
||||
save_alert: {
|
||||
|
|
Loading…
Add table
Reference in a new issue