mirror of
https://github.com/logto-io/logto.git
synced 2025-02-24 22:05:56 -05:00
chore: profile api test page
This commit is contained in:
parent
b210d8b59c
commit
b2ce6976cd
14 changed files with 191 additions and 103 deletions
|
@ -18,6 +18,7 @@ import CheckoutSuccessCallback from '@/pages/CheckoutSuccessCallback';
|
|||
import { dropLeadingSlash } from '@/utils/url';
|
||||
|
||||
import { __Internal__ImportError } from './internal';
|
||||
import HandleSocialCallback from '@/pages/Profile/containers/HandleSocialCallback';
|
||||
|
||||
const Welcome = safeLazy(async () => import('@/pages/Welcome'));
|
||||
const Profile = safeLazy(async () => import('@/pages/Profile'));
|
||||
|
@ -52,6 +53,7 @@ export function ConsoleRoutes() {
|
|||
)}
|
||||
<Route element={<ProtectedRoutes />}>
|
||||
<Route path={dropLeadingSlash(GlobalRoute.Profile) + '/*'} element={<Profile />} />
|
||||
<Route path={dropLeadingSlash(GlobalRoute.HandleSocial)} element={<HandleSocialCallback />} />
|
||||
<Route element={<TenantAccess />}>
|
||||
{isCloud && (
|
||||
<Route
|
||||
|
|
57
packages/console/src/hooks/use-account.ts
Normal file
57
packages/console/src/hooks/use-account.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import type { JsonObject, UserProfileResponse } from '@logto/schemas';
|
||||
import { useCallback } from 'react';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { adminTenantEndpoint, meApi } from '@/consts';
|
||||
|
||||
import type { RequestError } from './use-api';
|
||||
import { useStaticApi } from './use-api';
|
||||
import useSwrFetcher from './use-swr-fetcher';
|
||||
|
||||
const useAccount = () => {
|
||||
const { isAuthenticated } = useLogto();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator });
|
||||
const fetcher = useSwrFetcher<UserProfileResponse>(api);
|
||||
const {
|
||||
data: user,
|
||||
error,
|
||||
isLoading,
|
||||
mutate,
|
||||
} = useSWR<UserProfileResponse, RequestError>(isAuthenticated && 'me', fetcher);
|
||||
|
||||
const updateCustomData = useCallback(
|
||||
async (customData: JsonObject) => {
|
||||
if (!user) {
|
||||
toast.error(t('errors.unexpected_error'));
|
||||
return;
|
||||
}
|
||||
|
||||
await mutate({
|
||||
...user,
|
||||
customData: await api
|
||||
.patch(`me/custom-data`, {
|
||||
json: customData,
|
||||
})
|
||||
.json<JsonObject>(),
|
||||
});
|
||||
},
|
||||
[api, mutate, t, user]
|
||||
);
|
||||
|
||||
return {
|
||||
user,
|
||||
isLoading,
|
||||
error,
|
||||
isLoaded: !isLoading && !error,
|
||||
reload: mutate,
|
||||
customData: user?.customData,
|
||||
/** Patch (shallow merge) the custom data of the current user. */
|
||||
updateCustomData,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAccount;
|
|
@ -39,7 +39,7 @@ export class RequestError extends Error {
|
|||
export type StaticApiProps = {
|
||||
prefixUrl?: URL;
|
||||
hideErrorToast?: boolean | LogtoErrorCode[];
|
||||
resourceIndicator: string;
|
||||
resourceIndicator?: string;
|
||||
timeout?: number;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
@ -143,7 +143,7 @@ export const useStaticApi = ({
|
|||
beforeRequest: [
|
||||
async (request) => {
|
||||
if (isAuthenticated) {
|
||||
const accessToken = await (resourceIndicator.startsWith(organizationUrnPrefix)
|
||||
const accessToken = await (resourceIndicator?.startsWith(organizationUrnPrefix)
|
||||
? getOrganizationToken(getOrganizationIdFromUrn(resourceIndicator))
|
||||
: getAccessToken(resourceIndicator));
|
||||
request.headers.set('Authorization', `Bearer ${accessToken ?? ''}`);
|
||||
|
|
|
@ -12,7 +12,7 @@ import type { Row } from '../CardContent';
|
|||
import CardContent from '../CardContent';
|
||||
|
||||
type Props = {
|
||||
readonly user: UserProfileResponse;
|
||||
readonly user: Partial<UserProfileResponse>;
|
||||
readonly onUpdate?: () => void;
|
||||
};
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { ConnectorResponse, UserProfileResponse } from '@logto/schemas';
|
|||
import { generateStandardId } from '@logto/shared/universal';
|
||||
import type { Optional } from '@silverhand/essentials';
|
||||
import { appendPath, conditional } from '@silverhand/essentials';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import MailIcon from '@/assets/icons/mail.svg?react';
|
||||
|
@ -19,15 +19,16 @@ import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
|||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import { popupWindow } from '../../utils';
|
||||
import { parseLocationState, popupWindow } from '../../utils';
|
||||
import type { Row } from '../CardContent';
|
||||
import CardContent from '../CardContent';
|
||||
import NotSet from '../NotSet';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
readonly user: UserProfileResponse;
|
||||
readonly user: Partial<UserProfileResponse>;
|
||||
readonly connectors?: ConnectorResponse[];
|
||||
readonly onUpdate: () => void;
|
||||
};
|
||||
|
@ -37,24 +38,64 @@ function LinkAccountSection({ user, connectors, onUpdate }: Props) {
|
|||
const { navigate } = useTenantPathname();
|
||||
const theme = useTheme();
|
||||
const { show: showConfirm } = useConfirmModal();
|
||||
const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator });
|
||||
const api = useStaticApi({ prefixUrl: adminTenantEndpoint });
|
||||
const { state } = useLocation();
|
||||
const { verificationRecordId, connectorId } = parseLocationState(state);
|
||||
|
||||
const getSocialAuthorizationUri = useCallback(
|
||||
async (connectorId: string) => {
|
||||
async (connectorId: string, verificationRecordId: string) => {
|
||||
const adminTenantEndpointUrl = new URL(adminTenantEndpoint);
|
||||
const state = generateStandardId(8);
|
||||
const redirectUri = new URL(`/callback/${connectorId}`, adminTenantEndpointUrl).href;
|
||||
const { redirectTo } = await api
|
||||
.post('me/social/authorization-uri', { json: { connectorId, state, redirectUri } })
|
||||
.json<{ redirectTo: string }>();
|
||||
const { authorizationUri, verificationRecordId: newIdentifierVerificationRecordId } =
|
||||
await api
|
||||
.post('api/verifications/social', { json: { connectorId, state, redirectUri } })
|
||||
.json<{ authorizationUri: string; verificationRecordId: string }>();
|
||||
|
||||
sessionStorage.setItem(storageKeys.linkingSocialConnector, connectorId);
|
||||
sessionStorage.setItem(
|
||||
storageKeys.linkingSocialConnector,
|
||||
`${connectorId}:${verificationRecordId}:${newIdentifierVerificationRecordId}`
|
||||
);
|
||||
|
||||
return redirectTo;
|
||||
return { authorizationUri };
|
||||
},
|
||||
[api]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!verificationRecordId || !connectorId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleSocial = async () => {
|
||||
const { authorizationUri } = await getSocialAuthorizationUri(connectorId, verificationRecordId);
|
||||
// Profile page has been moved to the root path instead of being nested inside a tenant context.
|
||||
// Therefore, we don't need to use `getUrl` to prepend the tenant segment in the callback URL.
|
||||
// Also, link social is Cloud only, so no need to conditionally prepend the `ossConsolePath`, either.
|
||||
const callback = new URL('/console/handle-social', window.location.href).href;
|
||||
|
||||
const queries = new URLSearchParams({
|
||||
redirectTo: authorizationUri,
|
||||
connectorId,
|
||||
callback,
|
||||
});
|
||||
|
||||
const newWindow = popupWindow(
|
||||
appendPath(adminTenantEndpoint, `/springboard?${queries.toString()}`).href,
|
||||
'Link social account with Logto',
|
||||
600,
|
||||
640
|
||||
);
|
||||
|
||||
newWindow?.focus();
|
||||
|
||||
// Clear the location state
|
||||
navigate(window.location.pathname, { replace: true, state: {} });
|
||||
};
|
||||
|
||||
void handleSocial();
|
||||
}, [verificationRecordId]);
|
||||
|
||||
const tableInfo = useMemo((): Array<Row<Optional<SocialUserInfo>>> => {
|
||||
if (!connectors) {
|
||||
return [];
|
||||
|
@ -62,7 +103,7 @@ function LinkAccountSection({ user, connectors, onUpdate }: Props) {
|
|||
|
||||
return connectors.map(({ id, name, logo, logoDark, target }) => {
|
||||
const logoSrc = theme === Theme.Dark && logoDark ? logoDark : logo;
|
||||
const relatedUserDetails = user.identities[target]?.details;
|
||||
const relatedUserDetails = user.identities?.[target]?.details;
|
||||
|
||||
const socialUserInfo = socialUserInfoGuard.safeParse(relatedUserDetails);
|
||||
const hasLinked = socialUserInfo.success;
|
||||
|
@ -95,26 +136,10 @@ function LinkAccountSection({ user, connectors, onUpdate }: Props) {
|
|||
: {
|
||||
name: 'profile.link',
|
||||
handler: async () => {
|
||||
const authUri = await getSocialAuthorizationUri(id);
|
||||
// Profile page has been moved to the root path instead of being nested inside a tenant context.
|
||||
// Therefore, we don't need to use `getUrl` to prepend the tenant segment in the callback URL.
|
||||
// Also, link social is Cloud only, so no need to conditionally prepend the `ossConsolePath`, either.
|
||||
const callback = new URL('/handle-social', window.location.href).href;
|
||||
|
||||
const queries = new URLSearchParams({
|
||||
redirectTo: authUri,
|
||||
connectorId: id,
|
||||
callback,
|
||||
navigate('verify-password', {
|
||||
state: { action: 'linkSocial', connectorId: id },
|
||||
});
|
||||
|
||||
const newWindow = popupWindow(
|
||||
appendPath(adminTenantEndpoint, `/springboard?${queries.toString()}`).href,
|
||||
'Link social account with Logto',
|
||||
600,
|
||||
640
|
||||
);
|
||||
|
||||
newWindow?.focus();
|
||||
return;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -151,7 +176,7 @@ function LinkAccountSection({ user, connectors, onUpdate }: Props) {
|
|||
action: {
|
||||
name: user.primaryEmail ? 'profile.change' : 'profile.link',
|
||||
handler: () => {
|
||||
navigate('link-email', {
|
||||
navigate('verify-password', {
|
||||
state: { email: user.primaryEmail, action: 'changeEmail' },
|
||||
});
|
||||
},
|
||||
|
|
|
@ -33,11 +33,7 @@ type FormFields = {
|
|||
function BasicUserInfoUpdateModal({ field, value: initialValue, isOpen, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { show: showModal } = useConfirmModal();
|
||||
const api = useStaticApi({
|
||||
prefixUrl: adminTenantEndpoint,
|
||||
resourceIndicator: meApi.indicator,
|
||||
hideErrorToast: true,
|
||||
});
|
||||
const api = useStaticApi({ prefixUrl: adminTenantEndpoint, hideErrorToast: true });
|
||||
const {
|
||||
register,
|
||||
clearErrors,
|
||||
|
@ -85,7 +81,7 @@ function BasicUserInfoUpdateModal({ field, value: initialValue, isOpen, onClose
|
|||
clearErrors();
|
||||
void handleSubmit(async (data) => {
|
||||
try {
|
||||
await api.patch('me', { json: { [field]: data[field] } });
|
||||
await api.patch('api/profile', { json: { [field]: data[field] } });
|
||||
toast.success(t('profile.updated', { target: t(`profile.settings.${field}`) }));
|
||||
onClose();
|
||||
} catch (error: unknown) {
|
||||
|
|
|
@ -15,7 +15,8 @@ import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
|||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
|
||||
import ExperienceLikeModal from '../../components/ExperienceLikeModal';
|
||||
import { handleError } from '../../utils';
|
||||
import { handleError, parseLocationState } from '../../utils';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
type FormFields = {
|
||||
newPassword: string;
|
||||
|
@ -33,6 +34,8 @@ function ChangePasswordModal() {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { navigate } = useTenantPathname();
|
||||
const { show: showModal } = useConfirmModal();
|
||||
const { state } = useLocation();
|
||||
const { verificationRecordId } = parseLocationState(state);
|
||||
const {
|
||||
watch,
|
||||
reset,
|
||||
|
@ -48,7 +51,6 @@ function ChangePasswordModal() {
|
|||
const [showPassword, setShowPassword] = useState(false);
|
||||
const api = useStaticApi({
|
||||
prefixUrl: adminTenantEndpoint,
|
||||
resourceIndicator: meApi.indicator,
|
||||
hideErrorToast: true,
|
||||
});
|
||||
|
||||
|
@ -61,7 +63,9 @@ function ChangePasswordModal() {
|
|||
clearErrors();
|
||||
void handleSubmit(async ({ newPassword }) => {
|
||||
try {
|
||||
await api.post(`me/password`, { json: { password: newPassword } });
|
||||
await api.post(`api/profile/password`, {
|
||||
json: { password: newPassword, verificationRecordId: verificationRecordId ?? null },
|
||||
});
|
||||
toast.success(t('profile.password_changed'));
|
||||
onClose();
|
||||
} catch (error: unknown) {
|
||||
|
|
|
@ -13,16 +13,17 @@ function HandleSocialCallback() {
|
|||
const { show: showModal } = useConfirmModal();
|
||||
const api = useStaticApi({
|
||||
prefixUrl: adminTenantEndpoint,
|
||||
resourceIndicator: meApi.indicator,
|
||||
hideErrorToast: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const connectorId = sessionStorage.getItem(storageKeys.linkingSocialConnector);
|
||||
const [connectorId, verificationRecordId, newIdentifierVerificationRecordId] = String(
|
||||
sessionStorage.getItem(storageKeys.linkingSocialConnector)
|
||||
).split(':');
|
||||
sessionStorage.removeItem(storageKeys.linkingSocialConnector);
|
||||
|
||||
if (connectorId) {
|
||||
if (connectorId && verificationRecordId && newIdentifierVerificationRecordId) {
|
||||
const queries = new URLSearchParams(search);
|
||||
queries.set(
|
||||
'redirectUri',
|
||||
|
@ -31,7 +32,12 @@ function HandleSocialCallback() {
|
|||
const connectorData = Object.fromEntries(queries);
|
||||
|
||||
try {
|
||||
await api.post('me/social/link-identity', { json: { connectorId, connectorData } });
|
||||
await api.post('api/verifications/social/verify', {
|
||||
json: { verificationRecordId: newIdentifierVerificationRecordId, connectorData },
|
||||
});
|
||||
await api.post('api/profile/identities', {
|
||||
json: { verificationRecordId, newIdentifierVerificationRecordId },
|
||||
});
|
||||
|
||||
window.close();
|
||||
} catch (error: unknown) {
|
||||
|
|
|
@ -30,7 +30,8 @@ function LinkEmailModal() {
|
|||
} = useForm<EmailForm>({
|
||||
reValidateMode: 'onBlur',
|
||||
});
|
||||
const api = useStaticApi({ prefixUrl: adminTenantEndpoint, resourceIndicator: meApi.indicator });
|
||||
const api = useStaticApi({ prefixUrl: adminTenantEndpoint });
|
||||
const { email: currentEmail, verificationRecordId } = parseLocationState(state);
|
||||
|
||||
const onClose = () => {
|
||||
navigate('/profile');
|
||||
|
@ -40,15 +41,19 @@ function LinkEmailModal() {
|
|||
clearErrors();
|
||||
void handleSubmit(
|
||||
trySubmitSafe(async ({ email }) => {
|
||||
await api.post(`me/verification-codes`, { json: { email } });
|
||||
const { verificationRecordId: newVerificationRecordId } = await api
|
||||
.post(`api/verifications/verification-code`, {
|
||||
json: { identifier: { type: 'email', value: email } },
|
||||
})
|
||||
.json<{ verificationRecordId: string }>();
|
||||
reset();
|
||||
navigate('../verification-code', { state: { email, action: 'changeEmail' } });
|
||||
navigate('../verification-code', {
|
||||
state: { email, action: 'changeEmail', verificationRecordId, newVerificationRecordId },
|
||||
});
|
||||
})
|
||||
)();
|
||||
};
|
||||
|
||||
const { email: currentEmail } = parseLocationState(state);
|
||||
|
||||
return (
|
||||
<ExperienceLikeModal
|
||||
title="profile.link_account.link_email"
|
||||
|
|
|
@ -19,15 +19,6 @@ import { handleError, parseLocationState } from '../../utils';
|
|||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
const resendTimeout = 59;
|
||||
|
||||
const getTimeout = () => {
|
||||
const now = new Date();
|
||||
now.setSeconds(now.getSeconds() + resendTimeout);
|
||||
|
||||
return now;
|
||||
};
|
||||
|
||||
function VerificationCodeModal() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { navigate } = useTenantPathname();
|
||||
|
@ -38,15 +29,10 @@ function VerificationCodeModal() {
|
|||
const [error, setError] = useState<string>();
|
||||
const api = useStaticApi({
|
||||
prefixUrl: adminTenantEndpoint,
|
||||
resourceIndicator: meApi.indicator,
|
||||
hideErrorToast: true,
|
||||
});
|
||||
const { email, action } = parseLocationState(state);
|
||||
|
||||
const { seconds, isRunning, restart } = useTimer({
|
||||
autoStart: true,
|
||||
expiryTimestamp: getTimeout(),
|
||||
});
|
||||
const { email, action, verificationRecordId, newVerificationRecordId } =
|
||||
parseLocationState(state);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
navigate('/profile');
|
||||
|
@ -61,10 +47,22 @@ function VerificationCodeModal() {
|
|||
}
|
||||
|
||||
try {
|
||||
await api.post(`me/verification-codes/verify`, { json: { verificationCode, email } });
|
||||
await api.post(`api/verifications/verification-code/verify`, {
|
||||
json: {
|
||||
verificationId: newVerificationRecordId,
|
||||
identifier: { type: 'email', value: email },
|
||||
code: verificationCode,
|
||||
},
|
||||
});
|
||||
|
||||
if (action === 'changeEmail') {
|
||||
await api.patch('me', { json: { primaryEmail: email } });
|
||||
await api.post('api/profile/primary-email', {
|
||||
json: {
|
||||
email,
|
||||
verificationRecordId,
|
||||
newIdentifierVerificationRecordId: newVerificationRecordId,
|
||||
},
|
||||
});
|
||||
toast.success(t('profile.email_changed'));
|
||||
|
||||
onClose();
|
||||
|
@ -125,23 +123,6 @@ function VerificationCodeModal() {
|
|||
setError(undefined);
|
||||
}}
|
||||
/>
|
||||
{isRunning ? (
|
||||
<div className={styles.message}>
|
||||
{t('profile.code.resend_countdown', { countdown: seconds })}
|
||||
</div>
|
||||
) : (
|
||||
<TextLink
|
||||
className={styles.link}
|
||||
onClick={async () => {
|
||||
setCode([]);
|
||||
setError(undefined);
|
||||
await api.post(`me/verification-codes`, { json: { email } });
|
||||
restart(getTimeout(), true);
|
||||
}}
|
||||
>
|
||||
{t('profile.code.resend')}
|
||||
</TextLink>
|
||||
)}
|
||||
{action === 'changePassword' && (
|
||||
<TextLink
|
||||
className={styles.link}
|
||||
|
|
|
@ -39,11 +39,10 @@ function VerifyPasswordModal() {
|
|||
});
|
||||
const api = useStaticApi({
|
||||
prefixUrl: adminTenantEndpoint,
|
||||
resourceIndicator: meApi.indicator,
|
||||
hideErrorToast: true,
|
||||
});
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { email } = parseLocationState(state);
|
||||
const { email, action, connectorId } = parseLocationState(state);
|
||||
|
||||
const onClose = () => {
|
||||
navigate('/profile');
|
||||
|
@ -53,9 +52,19 @@ function VerifyPasswordModal() {
|
|||
clearErrors();
|
||||
void handleSubmit(async ({ password }) => {
|
||||
try {
|
||||
await api.post(`me/password/verify`, { json: { password } });
|
||||
const { verificationRecordId } = await api
|
||||
.post(`api/verifications/password`, { json: { password } })
|
||||
.json<{ verificationRecordId: string }>();
|
||||
reset();
|
||||
navigate('../change-password', { state });
|
||||
if (action === 'changeEmail') {
|
||||
navigate('../link-email', {
|
||||
state: { email, action, verificationRecordId },
|
||||
});
|
||||
} else if (action === 'linkSocial') {
|
||||
navigate('/profile', { state: { action, verificationRecordId, connectorId } });
|
||||
} else {
|
||||
navigate('../change-password', { state: { email, action, verificationRecordId } });
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
void handleError(error, async (code, message) => {
|
||||
if (code === 'session.invalid_credentials') {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import type { ConnectorResponse } from '@logto/schemas';
|
||||
import type { ConnectorResponse, UserProfileResponse } from '@logto/schemas';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
import useSWRImmutable from 'swr/immutable';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import FormCard from '@/components/FormCard';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
|
@ -13,10 +14,8 @@ import AppBoundary from '@/containers/AppBoundary';
|
|||
import Button from '@/ds-components/Button';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import type { RequestError } from '@/hooks/use-api';
|
||||
import { useStaticApi } from '@/hooks/use-api';
|
||||
import { useStaticApi, type RequestError } from '@/hooks/use-api';
|
||||
import { profile } from '@/hooks/use-console-routes/routes/profile';
|
||||
import useCurrentUser from '@/hooks/use-current-user';
|
||||
import { usePlausiblePageview } from '@/hooks/use-plausible-pageview';
|
||||
import useSwrFetcher from '@/hooks/use-swr-fetcher';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
|
@ -44,10 +43,13 @@ function Profile() {
|
|||
RequestError
|
||||
>('me/social/connectors', fetcher);
|
||||
const isLoadingConnectors = !connectors && !fetchConnectorsError;
|
||||
const { user, reload, isLoading: isLoadingUser } = useCurrentUser();
|
||||
const { isLoading: isUserAssetServiceLoading } = useUserAssetsService();
|
||||
const [showDeleteAccountModal, setShowDeleteAccountModal] = useState(false);
|
||||
|
||||
const profileApi = useStaticApi({ prefixUrl: adminTenantEndpoint });
|
||||
const profileFetcher = useSwrFetcher<Partial<UserProfileResponse>>(profileApi);
|
||||
const { data: user, mutate, isLoading: isLoadingUser } = useSWR('api/profile', profileFetcher);
|
||||
|
||||
// Avoid unnecessary re-renders in child components
|
||||
const show = useCallback(() => {
|
||||
setShowDeleteAccountModal(true);
|
||||
|
@ -73,10 +75,8 @@ function Profile() {
|
|||
{showLoadingSkeleton && <Skeleton />}
|
||||
{user && !showLoadingSkeleton && (
|
||||
<div className={styles.content}>
|
||||
<BasicUserInfoSection user={user} onUpdate={reload} />
|
||||
{isCloud && (
|
||||
<LinkAccountSection user={user} connectors={connectors} onUpdate={reload} />
|
||||
)}
|
||||
<BasicUserInfoSection user={user} onUpdate={mutate} />
|
||||
<LinkAccountSection user={user} connectors={connectors} onUpdate={mutate} />
|
||||
<FormCard title="profile.password.title">
|
||||
<CardContent
|
||||
title="profile.password.password_setting"
|
||||
|
|
|
@ -7,7 +7,7 @@ import { type LocationState, locationStateGuard } from '@/types/profile';
|
|||
export const parseLocationState = (state: unknown): Partial<LocationState> => {
|
||||
const parsed = locationStateGuard.safeParse(state);
|
||||
|
||||
return parsed.success ? parsed.data : { email: undefined, action: undefined };
|
||||
return parsed.success ? parsed.data : { email: undefined, action: undefined, verificationRecordId: undefined };
|
||||
};
|
||||
|
||||
export const popupWindow = (url: string, windowName: string, width: number, height: number) => {
|
||||
|
|
|
@ -2,8 +2,11 @@ import { emailRegEx } from '@logto/core-kit';
|
|||
import { z } from 'zod';
|
||||
|
||||
export const locationStateGuard = z.object({
|
||||
email: z.string().regex(emailRegEx),
|
||||
action: z.union([z.literal('changeEmail'), z.literal('changePassword')]),
|
||||
email: z.string().regex(emailRegEx).nullable().optional(),
|
||||
action: z.union([z.literal('changeEmail'), z.literal('changePassword'), z.literal('linkSocial')]),
|
||||
verificationRecordId: z.string().optional(),
|
||||
newVerificationRecordId: z.string().optional(),
|
||||
connectorId: z.string().optional(),
|
||||
});
|
||||
|
||||
export type LocationState = z.infer<typeof locationStateGuard>;
|
||||
|
|
Loading…
Add table
Reference in a new issue