mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-22 20:41:50 +08:00
feat(mobile): add delete account function (#12688)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a "Delete my account" option in mobile settings with role-based warnings and confirmation modals. - **Enhancements** - Added flexible row and reverse row layout options for modal footers and action buttons on mobile. - **Localization** - Added English translation for the "Delete my account" setting. - **Style** - Updated styles for modal footers and action buttons on mobile. - Added styling for account deletion dialog descriptions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -67,9 +67,16 @@ export const mobileStyles = {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
selectors: {
|
selectors: {
|
||||||
|
'&.row': {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
'&.reverse': {
|
'&.reverse': {
|
||||||
flexDirection: 'column-reverse',
|
flexDirection: 'column-reverse',
|
||||||
},
|
},
|
||||||
|
'&.rowReverse': {
|
||||||
|
flexDirection: 'row-reverse',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
action: style([
|
action: style([
|
||||||
@@ -80,6 +87,15 @@ export const mobileStyles = {
|
|||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
|
selectors: {
|
||||||
|
'&.row': {
|
||||||
|
width: 'auto',
|
||||||
|
minWidth: 0,
|
||||||
|
maxWidth: 140,
|
||||||
|
flex: 1,
|
||||||
|
height: 32,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ export interface ConfirmModalProps extends ModalProps {
|
|||||||
cancelText?: React.ReactNode;
|
cancelText?: React.ReactNode;
|
||||||
cancelButtonOptions?: Omit<ButtonProps, 'children'>;
|
cancelButtonOptions?: Omit<ButtonProps, 'children'>;
|
||||||
reverseFooter?: boolean;
|
reverseFooter?: boolean;
|
||||||
|
/**
|
||||||
|
* Whether to use row layout for mobile footer
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
rowFooter?: boolean;
|
||||||
/**
|
/**
|
||||||
* Auto focus on confirm button when modal opened
|
* Auto focus on confirm button when modal opened
|
||||||
* @default true
|
* @default true
|
||||||
@@ -45,6 +50,7 @@ export const ConfirmModal = ({
|
|||||||
descriptionClassName,
|
descriptionClassName,
|
||||||
childrenContentClassName,
|
childrenContentClassName,
|
||||||
contentOptions,
|
contentOptions,
|
||||||
|
rowFooter = false,
|
||||||
...props
|
...props
|
||||||
}: ConfirmModalProps) => {
|
}: ConfirmModalProps) => {
|
||||||
const onConfirmClick = useCallback(() => {
|
const onConfirmClick = useCallback(() => {
|
||||||
@@ -84,13 +90,17 @@ export const ConfirmModal = ({
|
|||||||
<div
|
<div
|
||||||
className={clsx(styles.footer, {
|
className={clsx(styles.footer, {
|
||||||
modalFooterWithChildren: !!children,
|
modalFooterWithChildren: !!children,
|
||||||
reverse: reverseFooter,
|
reverse: reverseFooter && !rowFooter,
|
||||||
|
row: rowFooter,
|
||||||
|
rowReverse: reverseFooter && rowFooter,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{onCancel !== false ? (
|
{onCancel !== false ? (
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className={styles.action}
|
className={clsx(styles.action, {
|
||||||
|
row: rowFooter,
|
||||||
|
})}
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
data-testid="confirm-modal-cancel"
|
data-testid="confirm-modal-cancel"
|
||||||
{...cancelButtonOptions}
|
{...cancelButtonOptions}
|
||||||
@@ -108,7 +118,9 @@ export const ConfirmModal = ({
|
|||||||
<CustomConfirmButton data-testid="confirm-modal-confirm" />
|
<CustomConfirmButton data-testid="confirm-modal-confirm" />
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
className={styles.action}
|
className={clsx(styles.action, {
|
||||||
|
row: rowFooter,
|
||||||
|
})}
|
||||||
onClick={onConfirmClick}
|
onClick={onConfirmClick}
|
||||||
data-testid="confirm-modal-confirm"
|
data-testid="confirm-modal-confirm"
|
||||||
autoFocus={autoFocusConfirm}
|
autoFocus={autoFocusConfirm}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const description = style({
|
||||||
|
fontSize: cssVar('fontBase'),
|
||||||
|
lineHeight: 1.6,
|
||||||
|
});
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { ConfirmModal, notify, useConfirmModal } from '@affine/component';
|
||||||
|
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||||
|
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 { useCallback, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { RowLayout } from '../row.layout';
|
||||||
|
import * as styles from './delete-account.css';
|
||||||
|
|
||||||
|
export const DeleteAccount = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
const workspacesService = useService(WorkspacesService);
|
||||||
|
const authService = useService(AuthService);
|
||||||
|
const session = authService.session;
|
||||||
|
const account = useLiveData(session.account$);
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
{account ? (
|
||||||
|
<RowLayout
|
||||||
|
label={t['com.affine.mobile.setting.others.delete-account']()}
|
||||||
|
onClick={openModal}
|
||||||
|
>
|
||||||
|
<ArrowRightSmallIcon fontSize={22} />
|
||||||
|
</RowLayout>
|
||||||
|
) : null}
|
||||||
|
{isTeamWorkspaceOwner ? (
|
||||||
|
<TeamOwnerWarningModal open={showModal} onOpenChange={setShowModal} />
|
||||||
|
) : (
|
||||||
|
<DeleteAccountModal open={showModal} onOpenChange={setShowModal} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { openConfirmModal } = useConfirmModal();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const onConfirm = useCallback(() => {
|
||||||
|
navigate('/');
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
const handleDeleteAccount = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
await authService.deleteAccount();
|
||||||
|
track.$.$.auth.deleteAccount();
|
||||||
|
openConfirmModal({
|
||||||
|
title: t['com.affine.setting.account.delete.success-title'](),
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{t['com.affine.setting.account.delete.success-description-1']()}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span>
|
||||||
|
{t['com.affine.setting.account.delete.success-description-2']()}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
cancelButtonOptions: {
|
||||||
|
style: {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
confirmText: t['Confirm'](),
|
||||||
|
onConfirm,
|
||||||
|
confirmButtonOptions: {
|
||||||
|
variant: 'primary',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = UserFriendlyError.fromAny(err);
|
||||||
|
notify.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [authService, onConfirm, openConfirmModal, t]);
|
||||||
|
|
||||||
|
const onDeleteAccountConfirm = useAsyncCallback(async () => {
|
||||||
|
await handleDeleteAccount();
|
||||||
|
}, [handleDeleteAccount]);
|
||||||
|
|
||||||
|
const onCancel = useCallback(() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
}, [onOpenChange]);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ConfirmModal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
title={t['com.affine.setting.account.delete.confirm-title']()}
|
||||||
|
description={
|
||||||
|
<Trans
|
||||||
|
i18nKey="com.affine.setting.account.delete.confirm-description-2"
|
||||||
|
components={{
|
||||||
|
1: <strong />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
confirmText={t['com.affine.setting.account.delete.confirm-button']()}
|
||||||
|
confirmButtonOptions={{
|
||||||
|
variant: 'error',
|
||||||
|
disabled: isLoading,
|
||||||
|
loading: isLoading,
|
||||||
|
onClick: onDeleteAccountConfirm,
|
||||||
|
}}
|
||||||
|
onCancel={onCancel}
|
||||||
|
cancelText={t['Cancel']()}
|
||||||
|
cancelButtonOptions={{
|
||||||
|
variant: 'primary',
|
||||||
|
}}
|
||||||
|
rowFooter
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import { useI18n } from '@affine/i18n';
|
|||||||
|
|
||||||
import { SettingGroup } from '../group';
|
import { SettingGroup } from '../group';
|
||||||
import { RowLayout } from '../row.layout';
|
import { RowLayout } from '../row.layout';
|
||||||
|
import { DeleteAccount } from './delete-account';
|
||||||
|
|
||||||
export const OthersGroup = () => {
|
export const OthersGroup = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -27,6 +28,7 @@ export const OthersGroup = () => {
|
|||||||
label={t['com.affine.mobile.setting.others.terms']()}
|
label={t['com.affine.mobile.setting.others.terms']()}
|
||||||
href="https://affine.pro/terms"
|
href="https://affine.pro/terms"
|
||||||
/>
|
/>
|
||||||
|
<DeleteAccount />
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2747,6 +2747,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Official website`
|
* `Official website`
|
||||||
*/
|
*/
|
||||||
["com.affine.mobile.setting.others.website"](): string;
|
["com.affine.mobile.setting.others.website"](): string;
|
||||||
|
/**
|
||||||
|
* `Delete my account`
|
||||||
|
*/
|
||||||
|
["com.affine.mobile.setting.others.delete-account"](): string;
|
||||||
/**
|
/**
|
||||||
* `Want to keep data local?`
|
* `Want to keep data local?`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -685,6 +685,7 @@
|
|||||||
"com.affine.mobile.setting.others.terms": "Terms of use",
|
"com.affine.mobile.setting.others.terms": "Terms of use",
|
||||||
"com.affine.mobile.setting.others.title": "Privacy & others",
|
"com.affine.mobile.setting.others.title": "Privacy & others",
|
||||||
"com.affine.mobile.setting.others.website": "Official website",
|
"com.affine.mobile.setting.others.website": "Official website",
|
||||||
|
"com.affine.mobile.setting.others.delete-account": "Delete my account",
|
||||||
"com.affine.mobile.sign-in.skip.hint": "Want to keep data local?",
|
"com.affine.mobile.sign-in.skip.hint": "Want to keep data local?",
|
||||||
"com.affine.mobile.sign-in.skip.link": "Start AFFiNE without an account",
|
"com.affine.mobile.sign-in.skip.link": "Start AFFiNE without an account",
|
||||||
"com.affine.moreThan30Days": "Older than a month",
|
"com.affine.moreThan30Days": "Older than a month",
|
||||||
|
|||||||
Reference in New Issue
Block a user