mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 03:56:23 +08:00
feat(core): add account deletion entry to account settings (#12385)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Implemented account deletion functionality with confirmation dialogs and success notifications. - Added a warning modal for team workspace owners before account deletion. - Introduced a new, richly formatted internationalized message for account deletion confirmation. - Added a new dialog component to inform users of successful account deletion. - **Improvements** - Updated localization strings to provide detailed guidance and warnings for account deletion. - Enhanced error handling by converting errors into user-friendly notifications. - Simplified and improved the sign-out process with better error handling and streamlined navigation. - **Style** - Added new style constants for success and warning modals related to account deletion. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const successDeleteAccountContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: '12px',
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ConfirmModal } from '@affine/component';
|
||||
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
|
||||
import type {
|
||||
DialogComponentProps,
|
||||
GLOBAL_DIALOG_SCHEMA,
|
||||
} from '@affine/core/modules/dialogs';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './index.css';
|
||||
export const DeletedAccountDialog = ({
|
||||
close,
|
||||
}: DialogComponentProps<GLOBAL_DIALOG_SCHEMA['deleted-account']>) => {
|
||||
const t = useI18n();
|
||||
const { jumpToIndex } = useNavigateHelper();
|
||||
const callback = useCallback(() => {
|
||||
jumpToIndex();
|
||||
}, [jumpToIndex]);
|
||||
|
||||
const handleOpenChange = useCallback(() => {
|
||||
callback();
|
||||
close();
|
||||
}, [callback, close]);
|
||||
return (
|
||||
<ConfirmModal
|
||||
open
|
||||
persistent
|
||||
title={t['com.affine.setting.account.delete.success-title']()}
|
||||
description={
|
||||
<span className={styles.successDeleteAccountContainer}>
|
||||
{t['com.affine.setting.account.delete.success-description-1']()}
|
||||
<span>
|
||||
{t['com.affine.setting.account.delete.success-description-2']()}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
confirmText={t['Confirm']()}
|
||||
onOpenChange={handleOpenChange}
|
||||
onConfirm={handleOpenChange}
|
||||
confirmButtonOptions={{
|
||||
variant: 'primary',
|
||||
}}
|
||||
cancelButtonOptions={{
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { ChangePasswordDialog } from './change-password';
|
||||
import { CollectionEditorDialog } from './collection-editor';
|
||||
import { CreateWorkspaceDialog } from './create-workspace';
|
||||
import { DeletedAccountDialog } from './deleted-account';
|
||||
import { DocInfoDialog } from './doc-info';
|
||||
import { EnableCloudDialog } from './enable-cloud';
|
||||
import { ImportDialog } from './import';
|
||||
@@ -31,6 +32,7 @@ const GLOBAL_DIALOGS = {
|
||||
'change-password': ChangePasswordDialog,
|
||||
'verify-email': VerifyEmailDialog,
|
||||
'enable-cloud': EnableCloudDialog,
|
||||
'deleted-account': DeletedAccountDialog,
|
||||
} satisfies {
|
||||
[key in keyof GLOBAL_DIALOG_SCHEMA]?: React.FC<
|
||||
DialogComponentProps<GLOBAL_DIALOG_SCHEMA[key]>
|
||||
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
import { ConfirmModal, Input, notify } from '@affine/component';
|
||||
import {
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { WorkspacesService } from '@affine/core/modules/workspace';
|
||||
import { UserFriendlyError } from '@affine/error';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import * as styles from './style.css';
|
||||
|
||||
export const DeleteAccount = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaceProfiles = workspacesService.getAllWorkspaceProfile();
|
||||
const isTeamWorkspaceOwner = workspaceProfiles.some(
|
||||
profile => profile.profile$.value?.isTeam && profile.profile$.value.isOwner
|
||||
);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const openModal = useCallback(() => {
|
||||
setShowModal(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SettingWrapper>
|
||||
<SettingRow
|
||||
name={
|
||||
<span style={{ color: cssVarV2('status/error') }}>
|
||||
{t['com.affine.setting.account.delete']()}
|
||||
</span>
|
||||
}
|
||||
desc={t['com.affine.setting.account.delete.message']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={openModal}
|
||||
data-testid="delete-account-button"
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
{isTeamWorkspaceOwner ? (
|
||||
<TeamOwnerWarningModal open={showModal} onOpenChange={setShowModal} />
|
||||
) : (
|
||||
<DeleteAccountModal open={showModal} onOpenChange={setShowModal} />
|
||||
)}
|
||||
</SettingWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const TeamOwnerWarningModal = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const onConfirm = useCallback(() => {
|
||||
onOpenChange(false);
|
||||
}, [onOpenChange]);
|
||||
return (
|
||||
<ConfirmModal
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t['com.affine.setting.account.delete.team-warning-title']()}
|
||||
description={t[
|
||||
'com.affine.setting.account.delete.team-warning-description'
|
||||
]()}
|
||||
confirmText={t['Confirm']()}
|
||||
confirmButtonOptions={{
|
||||
variant: 'primary',
|
||||
}}
|
||||
onConfirm={onConfirm}
|
||||
cancelButtonOptions={{
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteAccountModal = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const authService = useService(AuthService);
|
||||
const session = authService.session;
|
||||
const account = useLiveData(session.account$);
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleDeleteAccount = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await authService.deleteAccount();
|
||||
track.$.$.auth.deleteAccount();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
const error = UserFriendlyError.fromAny(err);
|
||||
notify.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authService]);
|
||||
|
||||
const onDeleteAccountConfirm = useCallback(async () => {
|
||||
await handleDeleteAccount();
|
||||
}, [handleDeleteAccount]);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ConfirmModal
|
||||
open={open}
|
||||
cancelText={t['com.affine.confirmModal.button.cancel']()}
|
||||
onConfirm={onDeleteAccountConfirm}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t['com.affine.setting.account.delete.confirm-title']()}
|
||||
description={t[
|
||||
'com.affine.setting.account.delete.confirm-description-1'
|
||||
]()}
|
||||
confirmText={t['com.affine.setting.account.delete.confirm-button']()}
|
||||
confirmButtonOptions={{
|
||||
variant: 'error',
|
||||
disabled: email !== account.email,
|
||||
loading: isLoading,
|
||||
}}
|
||||
childrenContentClassName={styles.confirmContent}
|
||||
>
|
||||
<Trans
|
||||
i18nKey="com.affine.setting.account.delete.confirm-description-2"
|
||||
components={{
|
||||
1: <strong />,
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t['com.affine.setting.account.delete.input-placeholder']()}
|
||||
value={email}
|
||||
onChange={setEmail}
|
||||
className={styles.inputWrapper}
|
||||
/>
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { FlexWrapper, Input, notify } from '@affine/component';
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
SettingWrapper,
|
||||
} from '@affine/component/setting-components';
|
||||
import { Avatar } from '@affine/component/ui/avatar';
|
||||
import { Button } from '@affine/component/ui/button';
|
||||
@@ -20,6 +21,7 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { AuthService, ServerService } from '../../../../modules/cloud';
|
||||
import type { SettingState } from '../types';
|
||||
import { AIUsagePanel } from './ai-usage-panel';
|
||||
import { DeleteAccount } from './delete-account';
|
||||
import { StorageProgress } from './storage-progress';
|
||||
import * as styles from './style.css';
|
||||
|
||||
@@ -214,51 +216,42 @@ export const AccountSetting = ({
|
||||
data-testid="account-title"
|
||||
/>
|
||||
<AvatarAndName />
|
||||
<SettingRow name={t['com.affine.settings.email']()} desc={account.email}>
|
||||
<Button onClick={onChangeEmail}>
|
||||
{account.info?.emailVerified
|
||||
? t['com.affine.settings.email.action.change']()
|
||||
: t['com.affine.settings.email.action.verify']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.password']()}
|
||||
desc={t['com.affine.settings.password.message']()}
|
||||
>
|
||||
<Button onClick={onPasswordButtonClick}>
|
||||
{account.info?.hasPassword
|
||||
? t['com.affine.settings.password.action.change']()
|
||||
: t['com.affine.settings.password.action.set']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<StoragePanel onChangeSettingState={onChangeSettingState} />
|
||||
{serverFeatures?.copilot && (
|
||||
<AIUsagePanel onChangeSettingState={onChangeSettingState} />
|
||||
)}
|
||||
<SettingRow
|
||||
name={t[`Sign out`]()}
|
||||
desc={t['com.affine.setting.sign.out.message']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
data-testid="sign-out-button"
|
||||
onClick={openSignOutModal}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
{/*<SettingRow*/}
|
||||
{/* name={*/}
|
||||
{/* <span style={{ color: 'var(--affine-warning-color)' }}>*/}
|
||||
{/* {t['com.affine.setting.account.delete']()}*/}
|
||||
{/* </span>*/}
|
||||
{/* }*/}
|
||||
{/* desc={t['com.affine.setting.account.delete.message']()}*/}
|
||||
{/* style={{ cursor: 'pointer' }}*/}
|
||||
{/* onClick={useCallback(() => {*/}
|
||||
{/* toast('Function coming soon');*/}
|
||||
{/* }, [])}*/}
|
||||
{/* testId="delete-account-button"*/}
|
||||
{/*>*/}
|
||||
{/* <ArrowRightSmallIcon />*/}
|
||||
{/*</SettingRow>*/}
|
||||
<SettingWrapper>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.email']()}
|
||||
desc={account.email}
|
||||
>
|
||||
<Button onClick={onChangeEmail}>
|
||||
{account.info?.emailVerified
|
||||
? t['com.affine.settings.email.action.change']()
|
||||
: t['com.affine.settings.email.action.verify']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
name={t['com.affine.settings.password']()}
|
||||
desc={t['com.affine.settings.password.message']()}
|
||||
>
|
||||
<Button onClick={onPasswordButtonClick}>
|
||||
{account.info?.hasPassword
|
||||
? t['com.affine.settings.password.action.change']()
|
||||
: t['com.affine.settings.password.action.set']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
<StoragePanel onChangeSettingState={onChangeSettingState} />
|
||||
{serverFeatures?.copilot && (
|
||||
<AIUsagePanel onChangeSettingState={onChangeSettingState} />
|
||||
)}
|
||||
<SettingRow
|
||||
name={t[`Sign out`]()}
|
||||
desc={t['com.affine.setting.sign.out.message']()}
|
||||
style={{ cursor: 'pointer' }}
|
||||
data-testid="sign-out-button"
|
||||
onClick={openSignOutModal}
|
||||
>
|
||||
<ArrowRightSmallIcon />
|
||||
</SettingRow>
|
||||
</SettingWrapper>
|
||||
<DeleteAccount />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,3 +39,17 @@ globalStyle(`${avatarWrapper} .camera-icon-wrapper`, {
|
||||
color: cssVar('white'),
|
||||
fontSize: cssVar('fontH4'),
|
||||
});
|
||||
|
||||
export const successDeleteAccountContainer = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: '12px',
|
||||
});
|
||||
export const confirmContent = style({
|
||||
paddingLeft: '0',
|
||||
paddingRight: '0',
|
||||
});
|
||||
export const inputWrapper = style({
|
||||
marginTop: '12px',
|
||||
});
|
||||
|
||||
+2
-1
@@ -11,6 +11,7 @@ import {
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { ArrowRightSmallIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
@@ -101,7 +102,7 @@ export const DeleteLeaveWorkspace = ({
|
||||
<>
|
||||
<SettingRow
|
||||
name={
|
||||
<span style={{ color: 'var(--affine-error-color)' }}>
|
||||
<span style={{ color: cssVarV2('status/error') }}>
|
||||
{isOwner
|
||||
? t['com.affine.workspaceDelete.title']()
|
||||
: t['com.affine.deleteLeaveWorkspace.leave']()}
|
||||
|
||||
Reference in New Issue
Block a user