mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
feat(console): sign in methods change alert (#701)
This commit is contained in:
parent
d815d96f1f
commit
a1ceea0685
7 changed files with 251 additions and 37 deletions
packages
console/src/pages/SignInExperience
phrases/src/locales
|
@ -0,0 +1,28 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font: var(--font-body-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-top: _.unit(6);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: _.unit(5);
|
||||||
|
background: var(--color-layer-2);
|
||||||
|
font: var(--font-body-medium);
|
||||||
|
|
||||||
|
.section {
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: _.unit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font: var(--font-subhead-2);
|
||||||
|
margin: _.unit(1) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connector {
|
||||||
|
margin-left: _.unit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { SignInExperience } from '@logto/schemas';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import Button from '@/components/Button';
|
||||||
|
import ModalLayout from '@/components/ModalLayout';
|
||||||
|
|
||||||
|
import * as styles from './SaveAlert.module.scss';
|
||||||
|
import SignInMethodsPreview from './SignInMethodsPreview';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
before: SignInExperience;
|
||||||
|
after: SignInExperience;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SaveAlert = ({ before, after, onClose, onConfirm }: Props) => {
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalLayout
|
||||||
|
title="sign_in_exp.save_alert.title"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button type="outline" title="general.cancel" onClick={onClose} />
|
||||||
|
<Button
|
||||||
|
type="danger"
|
||||||
|
title="general.confirm"
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
onConfirm();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className={styles.description}>{t('sign_in_exp.save_alert.description')}</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.title}>{t('sign_in_exp.save_alert.before')}</div>
|
||||||
|
<SignInMethodsPreview data={before} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.title}>{t('sign_in_exp.save_alert.after')}</div>
|
||||||
|
<SignInMethodsPreview data={after} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SaveAlert;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { ConnectorDTO, SignInExperience, SignInMethodKey, SignInMethodState } from '@logto/schemas';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||||
|
import { RequestError } from '@/hooks/use-api';
|
||||||
|
|
||||||
|
import * as styles from './SaveAlert.module.scss';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
data: SignInExperience;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SignInMethodsPreview = ({ data }: Props) => {
|
||||||
|
const { data: connectors, error } = useSWR<ConnectorDTO[], RequestError>('/api/connectors');
|
||||||
|
const { signInMethods, socialSignInConnectorIds } = data;
|
||||||
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
|
const connectorNames = useMemo(() => {
|
||||||
|
if (!connectors) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return socialSignInConnectorIds.map((connectorId) => {
|
||||||
|
const connector = connectors.find(({ id }) => id === connectorId);
|
||||||
|
|
||||||
|
if (!connector) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnnamedTrans
|
||||||
|
key={connectorId}
|
||||||
|
className={styles.connector}
|
||||||
|
resource={connector.metadata.name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [connectors, socialSignInConnectorIds]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{!connectors && !error && <div>loading</div>}
|
||||||
|
{error && <div>{error.body.message}</div>}
|
||||||
|
{connectors &&
|
||||||
|
Object.values(SignInMethodKey)
|
||||||
|
.filter((key) => signInMethods[key] !== SignInMethodState.Disabled)
|
||||||
|
.map((key) => (
|
||||||
|
<div key={key}>
|
||||||
|
{t('sign_in_exp.sign_in_methods.methods', { context: key })}
|
||||||
|
{key === SignInMethodKey.Social && <span>: {connectorNames}</span>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SignInMethodsPreview;
|
|
@ -1,9 +1,10 @@
|
||||||
import { Setting, SignInExperience as SignInExperienceType } from '@logto/schemas';
|
import { Setting, SignInExperience as SignInExperienceType } from '@logto/schemas';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'react-hot-toast';
|
import { toast } from 'react-hot-toast';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import ReactModal from 'react-modal';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
@ -13,25 +14,29 @@ import CardTitle from '@/components/CardTitle';
|
||||||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||||
import useApi, { RequestError } from '@/hooks/use-api';
|
import useApi, { RequestError } from '@/hooks/use-api';
|
||||||
import * as detailsStyles from '@/scss/details.module.scss';
|
import * as detailsStyles from '@/scss/details.module.scss';
|
||||||
|
import * as modalStyles from '@/scss/modal.module.scss';
|
||||||
|
|
||||||
import BrandingForm from './components/BrandingForm';
|
import BrandingForm from './components/BrandingForm';
|
||||||
import LanguagesForm from './components/LanguagesForm';
|
import LanguagesForm from './components/LanguagesForm';
|
||||||
|
import SaveAlert from './components/SaveAlert';
|
||||||
import SignInMethodsForm from './components/SignInMethodsForm';
|
import SignInMethodsForm from './components/SignInMethodsForm';
|
||||||
import TermsForm from './components/TermsForm';
|
import TermsForm from './components/TermsForm';
|
||||||
import Welcome from './components/Welcome';
|
import Welcome from './components/Welcome';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
import { SignInExperienceForm } from './types';
|
import { SignInExperienceForm } from './types';
|
||||||
import { signInExperienceParser } from './utilities';
|
import { compareSignInMethods, signInExperienceParser } from './utilities';
|
||||||
|
|
||||||
const SignInExperience = () => {
|
const SignInExperience = () => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
const { tab } = useParams();
|
const { tab } = useParams();
|
||||||
const { data, error, mutate } = useSWR<SignInExperienceType, RequestError>('/api/sign-in-exp');
|
const { data, error, mutate } = useSWR<SignInExperienceType, RequestError>('/api/sign-in-exp');
|
||||||
const { data: settings, error: settingsError } = useSWR<Setting, RequestError>('/api/settings');
|
const { data: settings, error: settingsError } = useSWR<Setting, RequestError>('/api/settings');
|
||||||
|
const [dataToCompare, setDataToCompare] = useState<SignInExperienceType>();
|
||||||
const methods = useForm<SignInExperienceForm>();
|
const methods = useForm<SignInExperienceForm>();
|
||||||
const {
|
const {
|
||||||
reset,
|
reset,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
getValues,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
} = methods;
|
} = methods;
|
||||||
const api = useApi();
|
const api = useApi();
|
||||||
|
@ -42,18 +47,31 @@ const SignInExperience = () => {
|
||||||
}
|
}
|
||||||
}, [data, reset]);
|
}, [data, reset]);
|
||||||
|
|
||||||
|
const saveData = async () => {
|
||||||
|
const updatedData = await api
|
||||||
|
.patch('/api/sign-in-exp', {
|
||||||
|
json: signInExperienceParser.toRemoteModel(getValues()),
|
||||||
|
})
|
||||||
|
.json<SignInExperienceType>();
|
||||||
|
void mutate(updatedData);
|
||||||
|
toast.success(t('application_details.save_success'));
|
||||||
|
};
|
||||||
|
|
||||||
const onSubmit = handleSubmit(async (formData) => {
|
const onSubmit = handleSubmit(async (formData) => {
|
||||||
if (!data || isSubmitting) {
|
if (!data || isSubmitting) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedData = await api
|
const formatted = signInExperienceParser.toRemoteModel(formData);
|
||||||
.patch('/api/sign-in-exp', {
|
|
||||||
json: signInExperienceParser.toRemoteModel(formData),
|
// Sign in methods changed, need to show confirm modal first.
|
||||||
})
|
if (!compareSignInMethods(data, formatted)) {
|
||||||
.json<SignInExperienceType>();
|
setDataToCompare(formatted);
|
||||||
void mutate(updatedData);
|
|
||||||
toast.success(t('application_details.save_success'));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveData();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!settings && !settingsError) {
|
if (!settings && !settingsError) {
|
||||||
|
@ -88,31 +106,51 @@ const SignInExperience = () => {
|
||||||
{error && <div>{`error occurred: ${error.body.message}`}</div>}
|
{error && <div>{`error occurred: ${error.body.message}`}</div>}
|
||||||
{data && (
|
{data && (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form className={classNames(detailsStyles.body, styles.form)} onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
{tab === 'experience' && (
|
<div className={classNames(detailsStyles.body, styles.form)}>
|
||||||
<>
|
{tab === 'experience' && (
|
||||||
<BrandingForm />
|
<>
|
||||||
<TermsForm />
|
<BrandingForm />
|
||||||
</>
|
<TermsForm />
|
||||||
)}
|
</>
|
||||||
{tab === 'methods' && <SignInMethodsForm />}
|
)}
|
||||||
{tab === 'others' && <LanguagesForm />}
|
{tab === 'methods' && <SignInMethodsForm />}
|
||||||
</form>
|
{tab === 'others' && <LanguagesForm />}
|
||||||
<div className={detailsStyles.footer}>
|
|
||||||
<div className={detailsStyles.footerMain}>
|
|
||||||
<Button
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
type="primary"
|
|
||||||
htmlType="submit"
|
|
||||||
title="general.save_changes"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className={detailsStyles.footer}>
|
||||||
|
<div className={detailsStyles.footerMain}>
|
||||||
|
<Button
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
title="general.save_changes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Card className={styles.preview}>TODO</Card>
|
<Card className={styles.preview}>TODO</Card>
|
||||||
|
{data && (
|
||||||
|
<ReactModal
|
||||||
|
isOpen={Boolean(dataToCompare)}
|
||||||
|
className={modalStyles.content}
|
||||||
|
overlayClassName={modalStyles.overlay}
|
||||||
|
>
|
||||||
|
{dataToCompare && (
|
||||||
|
<SaveAlert
|
||||||
|
before={data}
|
||||||
|
after={dataToCompare}
|
||||||
|
onClose={() => {
|
||||||
|
setDataToCompare(undefined);
|
||||||
|
}}
|
||||||
|
onConfirm={saveData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ReactModal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -76,3 +76,21 @@ export const signInExperienceParser = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const compareSignInMethods = (
|
||||||
|
before: SignInExperience,
|
||||||
|
after: SignInExperience
|
||||||
|
): boolean => {
|
||||||
|
if (before.socialSignInConnectorIds.length !== after.socialSignInConnectorIds.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (before.socialSignInConnectorIds.some((id) => !after.socialSignInConnectorIds.includes(id))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { signInMethods: beforeMethods } = before;
|
||||||
|
const { signInMethods: afterMethods } = after;
|
||||||
|
|
||||||
|
return Object.values(SignInMethodKey).every((key) => beforeMethods[key] === afterMethods[key]);
|
||||||
|
};
|
||||||
|
|
|
@ -11,6 +11,8 @@ const translation = {
|
||||||
loading: 'Loading...',
|
loading: 'Loading...',
|
||||||
redirecting: 'Redirecting...',
|
redirecting: 'Redirecting...',
|
||||||
added: 'Added',
|
added: 'Added',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
confirm: 'Confirm',
|
||||||
},
|
},
|
||||||
main_flow: {
|
main_flow: {
|
||||||
input: {
|
input: {
|
||||||
|
@ -423,6 +425,13 @@ const translation = {
|
||||||
no_connector:
|
no_connector:
|
||||||
'You haven’t set up any social connectors yet. Your sign in experience won’t go live until you finish the settings first. ',
|
'You haven’t set up any social connectors yet. Your sign in experience won’t go live until you finish the settings first. ',
|
||||||
},
|
},
|
||||||
|
save_alert: {
|
||||||
|
title: 'Reminder',
|
||||||
|
description:
|
||||||
|
'You are changing sign in methods from one to another. This will impact some of your users. Are you sure you want to do that?',
|
||||||
|
before: 'Before',
|
||||||
|
after: 'After',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
|
|
|
@ -13,6 +13,8 @@ const translation = {
|
||||||
loading: '读取中...',
|
loading: '读取中...',
|
||||||
redirecting: '页面跳转中...',
|
redirecting: '页面跳转中...',
|
||||||
added: '已添加',
|
added: '已添加',
|
||||||
|
cancel: '取消',
|
||||||
|
confirm: '确认',
|
||||||
},
|
},
|
||||||
main_flow: {
|
main_flow: {
|
||||||
input: {
|
input: {
|
||||||
|
@ -379,17 +381,17 @@ const translation = {
|
||||||
terms_of_use_placeholder: 'Terms of use url',
|
terms_of_use_placeholder: 'Terms of use url',
|
||||||
},
|
},
|
||||||
sign_in_methods: {
|
sign_in_methods: {
|
||||||
title: 'SIGN IN METHODS',
|
title: '登录方式',
|
||||||
primary: 'Primary sign in method',
|
primary: '首选登录方式',
|
||||||
enable_secondary: 'Enable secondary sign in',
|
enable_secondary: '启用其他登录方式',
|
||||||
enable_secondary_description:
|
enable_secondary_description:
|
||||||
"Once it's turned on, you app will support more sign in method(s) besides the primary one. ",
|
"Once it's turned on, you app will support more sign in method(s) besides the primary one. ",
|
||||||
methods: 'Sign in method',
|
methods: '登录方式',
|
||||||
methods_sms: 'Phone number sign in',
|
methods_sms: '短信验证码登录',
|
||||||
methods_email: 'Email sign in',
|
methods_email: '邮箱登录',
|
||||||
methods_social: 'Social sign in',
|
methods_social: '社交账号登录',
|
||||||
methods_username: 'Username-with-password sign in',
|
methods_username: '用户名密码登录',
|
||||||
methods_primary_tag: '(Primary)',
|
methods_primary_tag: '(首选)',
|
||||||
define_social_methods: 'Define social sign in methods',
|
define_social_methods: 'Define social sign in methods',
|
||||||
transfer: {
|
transfer: {
|
||||||
title: 'Social connectors',
|
title: 'Social connectors',
|
||||||
|
@ -418,6 +420,12 @@ const translation = {
|
||||||
setup: '设置',
|
setup: '设置',
|
||||||
no_connector: '你尚未添加社交登录连接器。你需要先完成设置才能启用。',
|
no_connector: '你尚未添加社交登录连接器。你需要先完成设置才能启用。',
|
||||||
},
|
},
|
||||||
|
save_alert: {
|
||||||
|
title: '提示',
|
||||||
|
description: '你正在修改登录方式,这可能会影响部分用户。是否继续保存修改?',
|
||||||
|
before: '修改前',
|
||||||
|
after: '修改后',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: '设置',
|
title: '设置',
|
||||||
|
|
Loading…
Add table
Reference in a new issue