-
Notifications
You must be signed in to change notification settings - Fork 8.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: support account deletion (#10008)
- Loading branch information
Showing
14 changed files
with
316 additions
and
346 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,11 +3,11 @@ import { useState } from 'react' | |
import { useTranslation } from 'react-i18next' | ||
|
||
import { useContext } from 'use-context-selector' | ||
import DeleteAccount from '../delete-account' | ||
import s from './index.module.css' | ||
import Collapse from '@/app/components/header/account-setting/collapse' | ||
import type { IItem } from '@/app/components/header/account-setting/collapse' | ||
import Modal from '@/app/components/base/modal' | ||
import Confirm from '@/app/components/base/confirm' | ||
import Button from '@/app/components/base/button' | ||
import { updateUserProfile } from '@/service/common' | ||
import { useAppContext } from '@/context/app-context' | ||
|
@@ -296,37 +296,9 @@ export default function AccountPage() { | |
} | ||
{ | ||
showDeleteAccountModal && ( | ||
<Confirm | ||
isShow | ||
<DeleteAccount | ||
onCancel={() => setShowDeleteAccountModal(false)} | ||
onConfirm={() => setShowDeleteAccountModal(false)} | ||
showCancel={false} | ||
type='warning' | ||
title={t('common.account.delete')} | ||
content={ | ||
<> | ||
<div className='my-1 text-text-destructive body-md-medium'> | ||
{t('common.account.deleteTip')} | ||
</div> | ||
<div className='mt-3 text-sm leading-5'> | ||
<span>{t('common.account.deleteConfirmTip')}</span> | ||
<a | ||
className='text-text-accent cursor' | ||
href={`mailto:[email protected]?subject=Delete Account Request&body=Delete Account: ${userProfile.email}`} | ||
target='_blank' | ||
rel='noreferrer noopener' | ||
onClick={(e) => { | ||
e.preventDefault() | ||
window.location.href = e.currentTarget.href | ||
}} | ||
> | ||
[email protected] | ||
</a> | ||
</div> | ||
<div className='my-2 px-3 py-2 rounded-lg bg-components-input-bg-active border border-components-input-border-active system-sm-regular text-components-input-text-filled'>{`${t('common.account.delete')}: ${userProfile.email}`}</div> | ||
</> | ||
} | ||
confirmText={t('common.operation.ok') as string} | ||
/> | ||
) | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
'use client' | ||
import { useTranslation } from 'react-i18next' | ||
import { useCallback, useState } from 'react' | ||
import Link from 'next/link' | ||
import { useSendDeleteAccountEmail } from '../state' | ||
import { useAppContext } from '@/context/app-context' | ||
import Input from '@/app/components/base/input' | ||
import Button from '@/app/components/base/button' | ||
|
||
type DeleteAccountProps = { | ||
onCancel: () => void | ||
onConfirm: () => void | ||
} | ||
|
||
export default function CheckEmail(props: DeleteAccountProps) { | ||
const { t } = useTranslation() | ||
const { userProfile } = useAppContext() | ||
const [userInputEmail, setUserInputEmail] = useState('') | ||
|
||
const { isPending: isSendingEmail, mutateAsync: getDeleteEmailVerifyCode } = useSendDeleteAccountEmail() | ||
|
||
const handleConfirm = useCallback(async () => { | ||
try { | ||
const ret = await getDeleteEmailVerifyCode() | ||
if (ret.result === 'success') | ||
props.onConfirm() | ||
} | ||
catch (error) { console.error(error) } | ||
}, [getDeleteEmailVerifyCode, props]) | ||
|
||
return <> | ||
<div className='py-1 text-text-destructive body-md-medium'> | ||
{t('common.account.deleteTip')} | ||
</div> | ||
<div className='pt-1 pb-2 text-text-secondary body-md-regular'> | ||
{t('common.account.deletePrivacyLinkTip')} | ||
<Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link> | ||
</div> | ||
<label className='mt-3 mb-1 h-6 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.deleteLabel')}</label> | ||
<Input placeholder={t('common.account.deletePlaceholder') as string} onChange={(e) => { | ||
setUserInputEmail(e.target.value) | ||
}} /> | ||
<div className='w-full flex flex-col mt-3 gap-2'> | ||
<Button className='w-full' disabled={userInputEmail !== userProfile.email || isSendingEmail} loading={isSendingEmail} variant='primary' onClick={handleConfirm}>{t('common.account.sendVerificationButton')}</Button> | ||
<Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button> | ||
</div> | ||
</> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
'use client' | ||
import { useTranslation } from 'react-i18next' | ||
import { useCallback, useState } from 'react' | ||
import { useRouter } from 'next/navigation' | ||
import { useDeleteAccountFeedback } from '../state' | ||
import { useAppContext } from '@/context/app-context' | ||
import Button from '@/app/components/base/button' | ||
import CustomDialog from '@/app/components/base/dialog' | ||
import Textarea from '@/app/components/base/textarea' | ||
import Toast from '@/app/components/base/toast' | ||
import { logout } from '@/service/common' | ||
|
||
type DeleteAccountProps = { | ||
onCancel: () => void | ||
onConfirm: () => void | ||
} | ||
|
||
export default function FeedBack(props: DeleteAccountProps) { | ||
const { t } = useTranslation() | ||
const { userProfile } = useAppContext() | ||
const router = useRouter() | ||
const [userFeedback, setUserFeedback] = useState('') | ||
const { isPending, mutateAsync: sendFeedback } = useDeleteAccountFeedback() | ||
|
||
const handleSuccess = useCallback(async () => { | ||
try { | ||
await logout({ | ||
url: '/logout', | ||
params: {}, | ||
}) | ||
localStorage.removeItem('refresh_token') | ||
localStorage.removeItem('console_token') | ||
router.push('/signin') | ||
Toast.notify({ type: 'info', message: t('common.account.deleteSuccessTip') }) | ||
} | ||
catch (error) { console.error(error) } | ||
}, [router, t]) | ||
|
||
const handleSubmit = useCallback(async () => { | ||
try { | ||
await sendFeedback({ feedback: userFeedback, email: userProfile.email }) | ||
props.onConfirm() | ||
await handleSuccess() | ||
} | ||
catch (error) { console.error(error) } | ||
}, [handleSuccess, userFeedback, sendFeedback, userProfile, props]) | ||
|
||
const handleSkip = useCallback(() => { | ||
props.onCancel() | ||
handleSuccess() | ||
}, [handleSuccess, props]) | ||
return <CustomDialog | ||
show={true} | ||
onClose={props.onCancel} | ||
title={t('common.account.feedbackTitle')} | ||
className="max-w-[480px]" | ||
footer={false} | ||
> | ||
<label className='mt-3 mb-1 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.feedbackLabel')}</label> | ||
<Textarea rows={6} value={userFeedback} placeholder={t('common.account.feedbackPlaceholder') as string} onChange={(e) => { | ||
setUserFeedback(e.target.value) | ||
}} /> | ||
<div className='w-full flex flex-col mt-3 gap-2'> | ||
<Button className='w-full' loading={isPending} variant='primary' onClick={handleSubmit}>{t('common.operation.submit')}</Button> | ||
<Button className='w-full' onClick={handleSkip}>{t('common.operation.skip')}</Button> | ||
</div> | ||
</CustomDialog> | ||
} |
55 changes: 55 additions & 0 deletions
55
web/app/account/delete-account/components/verify-email.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
'use client' | ||
import { useTranslation } from 'react-i18next' | ||
import { useCallback, useEffect, useState } from 'react' | ||
import Link from 'next/link' | ||
import { useAccountDeleteStore, useConfirmDeleteAccount, useSendDeleteAccountEmail } from '../state' | ||
import Input from '@/app/components/base/input' | ||
import Button from '@/app/components/base/button' | ||
import Countdown from '@/app/components/signin/countdown' | ||
|
||
const CODE_EXP = /[A-Za-z\d]{6}/gi | ||
|
||
type DeleteAccountProps = { | ||
onCancel: () => void | ||
onConfirm: () => void | ||
} | ||
|
||
export default function VerifyEmail(props: DeleteAccountProps) { | ||
const { t } = useTranslation() | ||
const emailToken = useAccountDeleteStore(state => state.sendEmailToken) | ||
const [verificationCode, setVerificationCode] = useState<string>() | ||
const [shouldButtonDisabled, setShouldButtonDisabled] = useState(true) | ||
const { mutate: sendEmail } = useSendDeleteAccountEmail() | ||
const { isPending: isDeleting, mutateAsync: confirmDeleteAccount } = useConfirmDeleteAccount() | ||
|
||
useEffect(() => { | ||
setShouldButtonDisabled(!(verificationCode && CODE_EXP.test(verificationCode)) || isDeleting) | ||
}, [verificationCode, isDeleting]) | ||
|
||
const handleConfirm = useCallback(async () => { | ||
try { | ||
const ret = await confirmDeleteAccount({ code: verificationCode!, token: emailToken }) | ||
if (ret.result === 'success') | ||
props.onConfirm() | ||
} | ||
catch (error) { console.error(error) } | ||
}, [emailToken, verificationCode, confirmDeleteAccount, props]) | ||
return <> | ||
<div className='pt-1 text-text-destructive body-md-medium'> | ||
{t('common.account.deleteTip')} | ||
</div> | ||
<div className='pt-1 pb-2 text-text-secondary body-md-regular'> | ||
{t('common.account.deletePrivacyLinkTip')} | ||
<Link href='https://dify.ai/privacy' className='text-text-accent'>{t('common.account.deletePrivacyLink')}</Link> | ||
</div> | ||
<label className='mt-3 mb-1 h-6 flex items-center system-sm-semibold text-text-secondary'>{t('common.account.verificationLabel')}</label> | ||
<Input minLength={6} maxLength={6} placeholder={t('common.account.verificationPlaceholder') as string} onChange={(e) => { | ||
setVerificationCode(e.target.value) | ||
}} /> | ||
<div className='w-full flex flex-col mt-3 gap-2'> | ||
<Button className='w-full' disabled={shouldButtonDisabled} loading={isDeleting} variant='warning' onClick={handleConfirm}>{t('common.account.permanentlyDeleteButton')}</Button> | ||
<Button className='w-full' onClick={props.onCancel}>{t('common.operation.cancel')}</Button> | ||
<Countdown onResend={sendEmail} /> | ||
</div> | ||
</> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
'use client' | ||
import { useTranslation } from 'react-i18next' | ||
import { useCallback, useState } from 'react' | ||
import CheckEmail from './components/check-email' | ||
import VerifyEmail from './components/verify-email' | ||
import FeedBack from './components/feed-back' | ||
import CustomDialog from '@/app/components/base/dialog' | ||
import { COUNT_DOWN_KEY, COUNT_DOWN_TIME_MS } from '@/app/components/signin/countdown' | ||
|
||
type DeleteAccountProps = { | ||
onCancel: () => void | ||
onConfirm: () => void | ||
} | ||
|
||
export default function DeleteAccount(props: DeleteAccountProps) { | ||
const { t } = useTranslation() | ||
|
||
const [showVerifyEmail, setShowVerifyEmail] = useState(false) | ||
const [showFeedbackDialog, setShowFeedbackDialog] = useState(false) | ||
|
||
const handleEmailCheckSuccess = useCallback(async () => { | ||
try { | ||
setShowVerifyEmail(true) | ||
localStorage.setItem(COUNT_DOWN_KEY, `${COUNT_DOWN_TIME_MS}`) | ||
} | ||
catch (error) { console.error(error) } | ||
}, []) | ||
|
||
if (showFeedbackDialog) | ||
return <FeedBack onCancel={props.onCancel} onConfirm={props.onConfirm} /> | ||
|
||
return <CustomDialog | ||
show={true} | ||
onClose={props.onCancel} | ||
title={t('common.account.delete')} | ||
className="max-w-[480px]" | ||
footer={false} | ||
> | ||
{!showVerifyEmail && <CheckEmail onCancel={props.onCancel} onConfirm={handleEmailCheckSuccess} />} | ||
{showVerifyEmail && <VerifyEmail onCancel={props.onCancel} onConfirm={() => { | ||
setShowFeedbackDialog(true) | ||
}} />} | ||
</CustomDialog> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { useMutation } from '@tanstack/react-query' | ||
import { create } from 'zustand' | ||
import { sendDeleteAccountCode, submitDeleteAccountFeedback, verifyDeleteAccountCode } from '@/service/common' | ||
|
||
type State = { | ||
sendEmailToken: string | ||
setSendEmailToken: (token: string) => void | ||
} | ||
|
||
export const useAccountDeleteStore = create<State>(set => ({ | ||
sendEmailToken: '', | ||
setSendEmailToken: (token: string) => set({ sendEmailToken: token }), | ||
})) | ||
|
||
export function useSendDeleteAccountEmail() { | ||
const updateEmailToken = useAccountDeleteStore(state => state.setSendEmailToken) | ||
return useMutation({ | ||
mutationKey: ['delete-account'], | ||
mutationFn: sendDeleteAccountCode, | ||
onSuccess: (ret) => { | ||
if (ret.result === 'success') | ||
updateEmailToken(ret.data) | ||
}, | ||
}) | ||
} | ||
|
||
export function useConfirmDeleteAccount() { | ||
return useMutation({ | ||
mutationKey: ['confirm-delete-account'], | ||
mutationFn: verifyDeleteAccountCode, | ||
}) | ||
} | ||
|
||
export function useDeleteAccountFeedback() { | ||
return useMutation({ | ||
mutationKey: ['delete-account-feedback'], | ||
mutationFn: submitDeleteAccountFeedback, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 0 additions & 9 deletions
9
web/app/components/header/account-setting/account-page/index.module.css
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.