mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -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
|
@ -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 classNames from 'classnames';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useSWR from 'swr';
|
||||
|
||||
|
@ -13,25 +14,29 @@ import CardTitle from '@/components/CardTitle';
|
|||
import TabNav, { TabNavLink } from '@/components/TabNav';
|
||||
import useApi, { RequestError } from '@/hooks/use-api';
|
||||
import * as detailsStyles from '@/scss/details.module.scss';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import BrandingForm from './components/BrandingForm';
|
||||
import LanguagesForm from './components/LanguagesForm';
|
||||
import SaveAlert from './components/SaveAlert';
|
||||
import SignInMethodsForm from './components/SignInMethodsForm';
|
||||
import TermsForm from './components/TermsForm';
|
||||
import Welcome from './components/Welcome';
|
||||
import * as styles from './index.module.scss';
|
||||
import { SignInExperienceForm } from './types';
|
||||
import { signInExperienceParser } from './utilities';
|
||||
import { compareSignInMethods, signInExperienceParser } from './utilities';
|
||||
|
||||
const SignInExperience = () => {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { tab } = useParams();
|
||||
const { data, error, mutate } = useSWR<SignInExperienceType, RequestError>('/api/sign-in-exp');
|
||||
const { data: settings, error: settingsError } = useSWR<Setting, RequestError>('/api/settings');
|
||||
const [dataToCompare, setDataToCompare] = useState<SignInExperienceType>();
|
||||
const methods = useForm<SignInExperienceForm>();
|
||||
const {
|
||||
reset,
|
||||
handleSubmit,
|
||||
getValues,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
const api = useApi();
|
||||
|
@ -42,18 +47,31 @@ const SignInExperience = () => {
|
|||
}
|
||||
}, [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) => {
|
||||
if (!data || isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedData = await api
|
||||
.patch('/api/sign-in-exp', {
|
||||
json: signInExperienceParser.toRemoteModel(formData),
|
||||
})
|
||||
.json<SignInExperienceType>();
|
||||
void mutate(updatedData);
|
||||
toast.success(t('application_details.save_success'));
|
||||
const formatted = signInExperienceParser.toRemoteModel(formData);
|
||||
|
||||
// Sign in methods changed, need to show confirm modal first.
|
||||
if (!compareSignInMethods(data, formatted)) {
|
||||
setDataToCompare(formatted);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await saveData();
|
||||
});
|
||||
|
||||
if (!settings && !settingsError) {
|
||||
|
@ -88,31 +106,51 @@ const SignInExperience = () => {
|
|||
{error && <div>{`error occurred: ${error.body.message}`}</div>}
|
||||
{data && (
|
||||
<FormProvider {...methods}>
|
||||
<form className={classNames(detailsStyles.body, styles.form)} onSubmit={onSubmit}>
|
||||
{tab === 'experience' && (
|
||||
<>
|
||||
<BrandingForm />
|
||||
<TermsForm />
|
||||
</>
|
||||
)}
|
||||
{tab === 'methods' && <SignInMethodsForm />}
|
||||
{tab === 'others' && <LanguagesForm />}
|
||||
</form>
|
||||
<div className={detailsStyles.footer}>
|
||||
<div className={detailsStyles.footerMain}>
|
||||
<Button
|
||||
isLoading={isSubmitting}
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
title="general.save_changes"
|
||||
/>
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className={classNames(detailsStyles.body, styles.form)}>
|
||||
{tab === 'experience' && (
|
||||
<>
|
||||
<BrandingForm />
|
||||
<TermsForm />
|
||||
</>
|
||||
)}
|
||||
{tab === 'methods' && <SignInMethodsForm />}
|
||||
{tab === 'others' && <LanguagesForm />}
|
||||
</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>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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...',
|
||||
redirecting: 'Redirecting...',
|
||||
added: 'Added',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
},
|
||||
main_flow: {
|
||||
input: {
|
||||
|
@ -423,6 +425,13 @@ const translation = {
|
|||
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. ',
|
||||
},
|
||||
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: {
|
||||
title: 'Settings',
|
||||
|
|
|
@ -13,6 +13,8 @@ const translation = {
|
|||
loading: '读取中...',
|
||||
redirecting: '页面跳转中...',
|
||||
added: '已添加',
|
||||
cancel: '取消',
|
||||
confirm: '确认',
|
||||
},
|
||||
main_flow: {
|
||||
input: {
|
||||
|
@ -379,17 +381,17 @@ const translation = {
|
|||
terms_of_use_placeholder: 'Terms of use url',
|
||||
},
|
||||
sign_in_methods: {
|
||||
title: 'SIGN IN METHODS',
|
||||
primary: 'Primary sign in method',
|
||||
enable_secondary: 'Enable secondary sign in',
|
||||
title: '登录方式',
|
||||
primary: '首选登录方式',
|
||||
enable_secondary: '启用其他登录方式',
|
||||
enable_secondary_description:
|
||||
"Once it's turned on, you app will support more sign in method(s) besides the primary one. ",
|
||||
methods: 'Sign in method',
|
||||
methods_sms: 'Phone number sign in',
|
||||
methods_email: 'Email sign in',
|
||||
methods_social: 'Social sign in',
|
||||
methods_username: 'Username-with-password sign in',
|
||||
methods_primary_tag: '(Primary)',
|
||||
methods: '登录方式',
|
||||
methods_sms: '短信验证码登录',
|
||||
methods_email: '邮箱登录',
|
||||
methods_social: '社交账号登录',
|
||||
methods_username: '用户名密码登录',
|
||||
methods_primary_tag: '(首选)',
|
||||
define_social_methods: 'Define social sign in methods',
|
||||
transfer: {
|
||||
title: 'Social connectors',
|
||||
|
@ -418,6 +420,12 @@ const translation = {
|
|||
setup: '设置',
|
||||
no_connector: '你尚未添加社交登录连接器。你需要先完成设置才能启用。',
|
||||
},
|
||||
save_alert: {
|
||||
title: '提示',
|
||||
description: '你正在修改登录方式,这可能会影响部分用户。是否继续保存修改?',
|
||||
before: '修改前',
|
||||
after: '修改后',
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
title: '设置',
|
||||
|
|
Loading…
Add table
Reference in a new issue