diff --git a/packages/console/src/containers/AppLayout/components/UserInfo/index.tsx b/packages/console/src/containers/AppLayout/components/UserInfo/index.tsx index 697a5a493..352ba1bbf 100644 --- a/packages/console/src/containers/AppLayout/components/UserInfo/index.tsx +++ b/packages/console/src/containers/AppLayout/components/UserInfo/index.tsx @@ -1,9 +1,8 @@ import { builtInLanguageOptions as consoleBuiltInLanguageOptions } from '@logto/phrases'; -import type { UserInfoResponse } from '@logto/react'; import { useLogto } from '@logto/react'; import { AppearanceMode } from '@logto/schemas'; import classNames from 'classnames'; -import { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -18,6 +17,7 @@ import { Ring as Spinner } from '@/components/Spinner'; import UserAvatar from '@/components/UserAvatar'; import UserInfoCard from '@/components/UserInfoCard'; import { getSignOutRedirectPathname } from '@/consts'; +import useLogtoAdminUser from '@/hooks/use-logto-admin-user'; import useUserPreferences from '@/hooks/use-user-preferences'; import { onKeyDownHandler } from '@/utils/a11y'; @@ -26,30 +26,19 @@ import UserInfoSkeleton from '../UserInfoSkeleton'; import * as styles from './index.module.scss'; const UserInfo = () => { - const { isAuthenticated, fetchUserInfo, signOut } = useLogto(); + const { signOut } = useLogto(); const navigate = useNavigate(); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const [user] = useLogtoAdminUser(); const anchorRef = useRef(null); const [showDropdown, setShowDropdown] = useState(false); - const [user, setUser] = - useState< - Pick & UserInfoResponse, 'username' | 'name' | 'picture' | 'email'> - >(); + const [isLoading, setIsLoading] = useState(false); const { data: { language, appearanceMode }, update, } = useUserPreferences(); - useEffect(() => { - (async () => { - if (isAuthenticated) { - const userInfo = await fetchUserInfo(); - setUser(userInfo ?? { name: '' }); // Provide a fallback to avoid infinite loading state - } - })(); - }, [isAuthenticated, fetchUserInfo]); - if (!user) { return ; } @@ -68,7 +57,7 @@ const UserInfo = () => { setShowDropdown(true); }} > - + , () => void, boolean] => { + const userId = useLogtoUserId(); + const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator }); + const fetcher = useSwrFetcher(api); + const { + data: user, + error, + mutate, + } = useSWR(userId && `me/users/${userId}`, fetcher); + + const isLoading = !user && !error; + + return [user, mutate, isLoading]; +}; + +export default useLogtoAdminUser; diff --git a/packages/console/src/hooks/use-logto-userinfo.ts b/packages/console/src/hooks/use-logto-userinfo.ts deleted file mode 100644 index dbbd564ad..000000000 --- a/packages/console/src/hooks/use-logto-userinfo.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { UserInfoResponse } from '@logto/react'; -import { useLogto } from '@logto/react'; -import type { Optional } from '@silverhand/essentials'; -import { useCallback, useEffect, useState } from 'react'; - -const useLogtoUserInfo = (): [Optional, () => void, boolean] => { - const { fetchUserInfo, isLoading, isAuthenticated } = useLogto(); - const [user, setUser] = useState(); - - const fetch = useCallback(async () => { - if (isAuthenticated) { - const userInfo = await fetchUserInfo(); - setUser(userInfo); - } else { - // eslint-disable-next-line unicorn/no-useless-undefined - setUser(undefined); - } - }, [fetchUserInfo, isAuthenticated]); - - useEffect(() => { - void fetch(); - }, [fetch]); - - return [user, fetch, isLoading]; -}; - -export default useLogtoUserInfo; diff --git a/packages/console/src/pages/Profile/components/LinkAccountSection/index.tsx b/packages/console/src/pages/Profile/components/LinkAccountSection/index.tsx index 14161e85b..3fc96aaca 100644 --- a/packages/console/src/pages/Profile/components/LinkAccountSection/index.tsx +++ b/packages/console/src/pages/Profile/components/LinkAccountSection/index.tsx @@ -4,6 +4,7 @@ import { AppearanceMode } from '@logto/schemas'; import type { Optional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { is } from 'superstruct'; @@ -13,6 +14,7 @@ import UnnamedTrans from '@/components/UnnamedTrans'; import UserInfoCard from '@/components/UserInfoCard'; import { adminTenantEndpoint, getBasename, meApi, profileSocialLinkingKeyPrefix } from '@/consts'; import { useStaticApi } from '@/hooks/use-api'; +import { useConfirmModal } from '@/hooks/use-confirm-modal'; import { useTheme } from '@/hooks/use-theme'; import type { SocialUserInfo } from '@/types/profile'; import { socialUserInfoGuard } from '@/types/profile'; @@ -30,8 +32,10 @@ type Props = { }; const LinkAccountSection = ({ user, onUpdate }: Props) => { + const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const navigate = useNavigate(); const theme = useTheme(); + const { show: showConfirm } = useConfirmModal(); const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator }); const [connectors, setConnectors] = useState(); @@ -72,8 +76,19 @@ const LinkAccountSection = ({ user, onUpdate }: Props) => { { name: 'profile.unlink', handler: async () => { - await api.delete(`me/social/identity/${id}`); - onUpdate(); + const [result] = await showConfirm({ + ModalContent: () => ( + }}> + {t('profile.unlink_reminder')} + + ), + confirmButtonText: 'profile.unlink_confirm_text', + }); + + if (result) { + await api.delete(`me/social/identity/${id}`); + onUpdate(); + } }, }, ] @@ -115,7 +130,16 @@ const LinkAccountSection = ({ user, onUpdate }: Props) => { ], }; }); - }, [connectors, theme, user.identities, api, onUpdate, getSocialAuthorizationUri]); + }, [ + api, + connectors, + theme, + user.identities, + showConfirm, + t, + onUpdate, + getSocialAuthorizationUri, + ]); return (
@@ -126,12 +150,15 @@ const LinkAccountSection = ({ user, onUpdate }: Props) => { key: 'email', label: 'profile.link_account.email', value: user.primaryEmail, - renderer: (email) => ( -
- - {email} -
- ), + renderer: (email) => + email ? ( +
+ + {email} +
+ ) : ( + + ), action: { name: 'profile.change', handler: () => { diff --git a/packages/console/src/pages/Profile/components/MainFlowLikeModal/index.module.scss b/packages/console/src/pages/Profile/components/MainFlowLikeModal/index.module.scss index 147ba0fe6..3cc18a5c6 100644 --- a/packages/console/src/pages/Profile/components/MainFlowLikeModal/index.module.scss +++ b/packages/console/src/pages/Profile/components/MainFlowLikeModal/index.module.scss @@ -28,7 +28,12 @@ display: flex; align-items: center; + &:active { + color: var(--color-primary-pressed); + } + &:not(:disabled):hover { + color: var(--color-primary-hover); text-decoration: none; } } diff --git a/packages/console/src/pages/Profile/components/Section/index.module.scss b/packages/console/src/pages/Profile/components/Section/index.module.scss index 076733e49..1faddede0 100644 --- a/packages/console/src/pages/Profile/components/Section/index.module.scss +++ b/packages/console/src/pages/Profile/components/Section/index.module.scss @@ -3,7 +3,6 @@ .container { padding: _.unit(6) _.unit(8); display: flex; - margin-top: _.unit(4); background: var(--color-layer-1); border-radius: 12px; } diff --git a/packages/console/src/pages/Profile/containers/BasicUserInfoUpdateModal/index.tsx b/packages/console/src/pages/Profile/containers/BasicUserInfoUpdateModal/index.tsx index f075d6efa..e8206fbf0 100644 --- a/packages/console/src/pages/Profile/containers/BasicUserInfoUpdateModal/index.tsx +++ b/packages/console/src/pages/Profile/containers/BasicUserInfoUpdateModal/index.tsx @@ -1,6 +1,7 @@ import type { AdminConsoleKey } from '@logto/phrases'; import { useEffect } from 'react'; import { useForm } from 'react-hook-form'; +import { toast } from 'react-hot-toast'; import { useTranslation } from 'react-i18next'; import ReactModal from 'react-modal'; @@ -72,6 +73,7 @@ const BasicUserInfoUpdateModal = ({ field, value: initialValue, isOpen, onClose clearErrors(); void handleSubmit(async (data) => { await api.patch(`me/user`, { json: { [field]: data[field] } }); + toast.success(t('profile.updated', { target: t(`profile.settings.${field}`) })); onClose(); })(); }; @@ -82,9 +84,7 @@ const BasicUserInfoUpdateModal = ({ field, value: initialValue, isOpen, onClose isOpen={isOpen} className={modalStyles.content} overlayClassName={modalStyles.overlay} - onRequestClose={() => { - onClose(); - }} + onRequestClose={onClose} > } - onClose={() => { - onClose(); - }} + onClose={onClose} >
{ checked={showPassword} label={t('profile.password.show_password')} onChange={() => { - setShowPassword((show) => !show); + setShowPassword((value) => !value); }} />
); }; diff --git a/packages/phrases/src/locales/de/translation/admin-console/profile.ts b/packages/phrases/src/locales/de/translation/admin-console/profile.ts index 9875adf6e..f487682a4 100644 --- a/packages/phrases/src/locales/de/translation/admin-console/profile.ts +++ b/packages/phrases/src/locales/de/translation/admin-console/profile.ts @@ -70,6 +70,11 @@ const profile = { updated: '{{target}} updated!', // UNTRANSLATED linked: '{{target}} linked!', // UNTRANSLATED unlinked: '{{target}} unlinked!', // UNTRANSLATED + email_exists_reminder: + 'This email {{email}} is associated with an existing account. Link another email here.', // UNTRANSLATED + unlink_confirm_text: 'Yes, unlink', // UNTRANSLATED + unlink_reminder: + 'You won’t be able to sign in with the account if you unlink it. Are you sure to proceed?', // UNTRANSLATED }; export default profile; diff --git a/packages/phrases/src/locales/en/translation/admin-console/profile.ts b/packages/phrases/src/locales/en/translation/admin-console/profile.ts index fa25e1053..e647b5c35 100644 --- a/packages/phrases/src/locales/en/translation/admin-console/profile.ts +++ b/packages/phrases/src/locales/en/translation/admin-console/profile.ts @@ -70,6 +70,11 @@ const profile = { updated: '{{target}} updated!', linked: '{{target}} linked!', unlinked: '{{target}} unlinked!', + email_exists_reminder: + 'This email {{email}} is associated with an existing account. Link another email here.', + unlink_confirm_text: 'Yes, unlink', + unlink_reminder: + 'You won’t be able to sign in with the account if you unlink it. Are you sure to proceed?', }; export default profile; diff --git a/packages/phrases/src/locales/fr/translation/admin-console/profile.ts b/packages/phrases/src/locales/fr/translation/admin-console/profile.ts index 9875adf6e..f487682a4 100644 --- a/packages/phrases/src/locales/fr/translation/admin-console/profile.ts +++ b/packages/phrases/src/locales/fr/translation/admin-console/profile.ts @@ -70,6 +70,11 @@ const profile = { updated: '{{target}} updated!', // UNTRANSLATED linked: '{{target}} linked!', // UNTRANSLATED unlinked: '{{target}} unlinked!', // UNTRANSLATED + email_exists_reminder: + 'This email {{email}} is associated with an existing account. Link another email here.', // UNTRANSLATED + unlink_confirm_text: 'Yes, unlink', // UNTRANSLATED + unlink_reminder: + 'You won’t be able to sign in with the account if you unlink it. Are you sure to proceed?', // UNTRANSLATED }; export default profile; diff --git a/packages/phrases/src/locales/ko/translation/admin-console/profile.ts b/packages/phrases/src/locales/ko/translation/admin-console/profile.ts index 9875adf6e..f487682a4 100644 --- a/packages/phrases/src/locales/ko/translation/admin-console/profile.ts +++ b/packages/phrases/src/locales/ko/translation/admin-console/profile.ts @@ -70,6 +70,11 @@ const profile = { updated: '{{target}} updated!', // UNTRANSLATED linked: '{{target}} linked!', // UNTRANSLATED unlinked: '{{target}} unlinked!', // UNTRANSLATED + email_exists_reminder: + 'This email {{email}} is associated with an existing account. Link another email here.', // UNTRANSLATED + unlink_confirm_text: 'Yes, unlink', // UNTRANSLATED + unlink_reminder: + 'You won’t be able to sign in with the account if you unlink it. Are you sure to proceed?', // UNTRANSLATED }; export default profile; diff --git a/packages/phrases/src/locales/pt-br/translation/admin-console/profile.ts b/packages/phrases/src/locales/pt-br/translation/admin-console/profile.ts index 9875adf6e..f487682a4 100644 --- a/packages/phrases/src/locales/pt-br/translation/admin-console/profile.ts +++ b/packages/phrases/src/locales/pt-br/translation/admin-console/profile.ts @@ -70,6 +70,11 @@ const profile = { updated: '{{target}} updated!', // UNTRANSLATED linked: '{{target}} linked!', // UNTRANSLATED unlinked: '{{target}} unlinked!', // UNTRANSLATED + email_exists_reminder: + 'This email {{email}} is associated with an existing account. Link another email here.', // UNTRANSLATED + unlink_confirm_text: 'Yes, unlink', // UNTRANSLATED + unlink_reminder: + 'You won’t be able to sign in with the account if you unlink it. Are you sure to proceed?', // UNTRANSLATED }; export default profile; diff --git a/packages/phrases/src/locales/pt-pt/translation/admin-console/profile.ts b/packages/phrases/src/locales/pt-pt/translation/admin-console/profile.ts index 9875adf6e..f487682a4 100644 --- a/packages/phrases/src/locales/pt-pt/translation/admin-console/profile.ts +++ b/packages/phrases/src/locales/pt-pt/translation/admin-console/profile.ts @@ -70,6 +70,11 @@ const profile = { updated: '{{target}} updated!', // UNTRANSLATED linked: '{{target}} linked!', // UNTRANSLATED unlinked: '{{target}} unlinked!', // UNTRANSLATED + email_exists_reminder: + 'This email {{email}} is associated with an existing account. Link another email here.', // UNTRANSLATED + unlink_confirm_text: 'Yes, unlink', // UNTRANSLATED + unlink_reminder: + 'You won’t be able to sign in with the account if you unlink it. Are you sure to proceed?', // UNTRANSLATED }; export default profile; diff --git a/packages/phrases/src/locales/tr-tr/translation/admin-console/profile.ts b/packages/phrases/src/locales/tr-tr/translation/admin-console/profile.ts index 9875adf6e..f487682a4 100644 --- a/packages/phrases/src/locales/tr-tr/translation/admin-console/profile.ts +++ b/packages/phrases/src/locales/tr-tr/translation/admin-console/profile.ts @@ -70,6 +70,11 @@ const profile = { updated: '{{target}} updated!', // UNTRANSLATED linked: '{{target}} linked!', // UNTRANSLATED unlinked: '{{target}} unlinked!', // UNTRANSLATED + email_exists_reminder: + 'This email {{email}} is associated with an existing account. Link another email here.', // UNTRANSLATED + unlink_confirm_text: 'Yes, unlink', // UNTRANSLATED + unlink_reminder: + 'You won’t be able to sign in with the account if you unlink it. Are you sure to proceed?', // UNTRANSLATED }; export default profile; diff --git a/packages/phrases/src/locales/zh-cn/translation/admin-console/profile.ts b/packages/phrases/src/locales/zh-cn/translation/admin-console/profile.ts index 7aeb545c3..7339a7f5d 100644 --- a/packages/phrases/src/locales/zh-cn/translation/admin-console/profile.ts +++ b/packages/phrases/src/locales/zh-cn/translation/admin-console/profile.ts @@ -67,6 +67,9 @@ const profile = { updated: '{{target}}更改成功!', linked: '{{target}}账号绑定成功!', unlinked: '{{target}}账号解绑成功!', + email_exists_reminder: '该邮箱 {{email}} 已被其他账号绑定,请更换邮箱。', + unlink_confirm_text: '确定解绑', + unlink_reminder: '解绑后,你将无法使用该 账号进行登录。确定要解绑吗?', }; export default profile;