From c8646245436ca988df4fa242f8b6a8a7ba86c920 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 22 Apr 2025 16:21:20 +0300 Subject: [PATCH] new delete account dialog --- src/screens/Settings/AccountSettings.tsx | 10 +- .../components/DeactivateAccountDialog.tsx | 13 +- .../components/DeleteAccountDialog.tsx | 255 ++++++++++++++++++ 3 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 src/screens/Settings/components/DeleteAccountDialog.tsx diff --git a/src/screens/Settings/AccountSettings.tsx b/src/screens/Settings/AccountSettings.tsx index 800880ca5c..8402d429ec 100644 --- a/src/screens/Settings/AccountSettings.tsx +++ b/src/screens/Settings/AccountSettings.tsx @@ -3,8 +3,8 @@ import {useLingui} from '@lingui/react' import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type CommonNavigatorParams} from '#/lib/routes/types' -import {useModalControls} from '#/state/modals' import {useSession} from '#/state/session' +import {DeleteAccountDialog} from '#/screens/Settings/components/DeleteAccountDialog' import * as SettingsList from '#/screens/Settings/components/SettingsList' import {atoms as a, useTheme} from '#/alf' import {AgeAssuranceAccountCard} from '#/components/ageAssurance/AgeAssuranceAccountCard' @@ -34,11 +34,11 @@ export function AccountSettingsScreen({}: Props) { const t = useTheme() const {_} = useLingui() const {currentAccount} = useSession() - const {openModal} = useModalControls() const emailDialogControl = useEmailDialogControl() const birthdayControl = useDialogControl() const changeHandleControl = useDialogControl() const changePasswordControl = useDialogControl() + const deleteAccountControl = useDialogControl() const exportCarControl = useDialogControl() const deactivateAccountControl = useDialogControl() @@ -169,7 +169,7 @@ export function AccountSettingsScreen({}: Props) { openModal({name: 'delete-account'})} + onPress={() => deleteAccountControl.open()} destructive> @@ -183,6 +183,10 @@ export function AccountSettingsScreen({}: Props) { + diff --git a/src/screens/Settings/components/DeactivateAccountDialog.tsx b/src/screens/Settings/components/DeactivateAccountDialog.tsx index 4570062ce9..19cccd2223 100644 --- a/src/screens/Settings/components/DeactivateAccountDialog.tsx +++ b/src/screens/Settings/components/DeactivateAccountDialog.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import {useCallback, useState} from 'react' import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -7,7 +7,6 @@ import {logger} from '#/logger' import {useAgent, useSessionApi} from '#/state/session' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import {type DialogOuterProps} from '#/components/Dialog' import {Divider} from '#/components/Divider' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' import {Loader} from '#/components/Loader' @@ -17,7 +16,7 @@ import {Text} from '#/components/Typography' export function DeactivateAccountDialog({ control, }: { - control: DialogOuterProps['control'] + control: Prompt.PromptControlProps }) { return ( @@ -29,17 +28,17 @@ export function DeactivateAccountDialog({ function DeactivateAccountDialogInner({ control, }: { - control: DialogOuterProps['control'] + control: Prompt.PromptControlProps }) { const t = useTheme() const {gtMobile} = useBreakpoints() const {_} = useLingui() const agent = useAgent() const {logoutCurrentAccount} = useSessionApi() - const [pending, setPending] = React.useState(false) - const [error, setError] = React.useState() + const [pending, setPending] = useState(false) + const [error, setError] = useState() - const handleDeactivate = React.useCallback(async () => { + const handleDeactivate = useCallback(async () => { try { setPending(true) await agent.com.atproto.server.deactivateAccount({}) diff --git a/src/screens/Settings/components/DeleteAccountDialog.tsx b/src/screens/Settings/components/DeleteAccountDialog.tsx new file mode 100644 index 0000000000..0e67d8f971 --- /dev/null +++ b/src/screens/Settings/components/DeleteAccountDialog.tsx @@ -0,0 +1,255 @@ +import {useState} from 'react' +import {Text as RNText, View} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {cleanError} from '#/lib/strings/errors' +import {sanitizeHandle} from '#/lib/strings/handles' +import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' +import {useAgent, useSession, useSessionApi} from '#/state/session' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' +import * as Toast from '#/view/com/util/Toast' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {Admonition} from '#/components/Admonition' +import {Button, ButtonIcon, ButtonText} from '#/components/Button' +import * as Dialog from '#/components/Dialog' +import * as TextField from '#/components/forms/TextField' +import {Loader} from '#/components/Loader' +import {Text} from '#/components/Typography' +import {resetToTab} from '#/Navigation' + +enum Stages { + SendEmail = 'SendEmail', + DeleteAccount = 'DeleteAccount', +} + +export function DeleteAccountDialog({ + control, + deactivateDialogControl, +}: { + control: Dialog.DialogControlProps + deactivateDialogControl: Dialog.DialogControlProps +}) { + return ( + + + + + ) +} + +function Inner({ + deactivateDialogControl, +}: { + deactivateDialogControl: Dialog.DialogControlProps +}) { + const {_} = useLingui() + const {currentAccount} = useSession() + const {removeAccount} = useSessionApi() + const agent = useAgent() + const control = Dialog.useDialogContext() + const t = useTheme() + const {gtMobile} = useBreakpoints() + + const [stage, setStage] = useState(Stages.SendEmail) + const [isProcessing, setIsProcessing] = useState(false) + const [confirmCode, setConfirmCode] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + + const onPressSendEmail = async () => { + setError('') + setIsProcessing(true) + try { + await agent.com.atproto.server.requestAccountDelete() + setStage(Stages.DeleteAccount) + } catch (e: any) { + setError(cleanError(e)) + } + setIsProcessing(false) + } + + const onPressConfirmDelete = async () => { + if (!currentAccount?.did) { + throw new Error(`DeleteAccount modal: currentAccount.did is undefined`) + } + + setError('') + setIsProcessing(true) + const token = confirmCode.replace(/\s/g, '') + + try { + // inform chat service of intent to delete account + const {success} = await agent.api.chat.bsky.actor.deleteAccount( + undefined, + { + headers: DM_SERVICE_HEADERS, + }, + ) + if (!success) { + throw new Error('Failed to inform chat service of account deletion') + } + await agent.com.atproto.server.deleteAccount({ + did: currentAccount.did, + password, + token, + }) + Toast.show(_(msg`Your account has been deleted`)) + resetToTab('HomeTab') + removeAccount(currentAccount) + control.close() + } catch (e: any) { + setError(cleanError(e)) + } + setIsProcessing(false) + } + + const title = + stage === Stages.SendEmail + ? currentAccount + ? _(msg`Delete account "${sanitizeHandle(currentAccount?.handle)}"`) + : _(msg`Delete account`) + : _(msg`Confirm deletion`) + + return ( + + + + {title} + + {error ? ( + + + + ) : null} + + + {stage === Stages.SendEmail ? ( + + For security reasons, we'll need to send a confirmation code to + your email address. + + ) : ( + + Check your inbox for an email with the confirmation code to + enter below: + + )} + + + + {stage === Stages.SendEmail && ( + + + You can also{' '} + + control.close(() => deactivateDialogControl.open()) + }> + temporarily deactivate + {' '} + your account instead, and reactivate it at any time. + + + )} + + {stage === Stages.DeleteAccount && ( + + + + Confirmation Code + + + + + + + + Password + + + + + + + )} + + + {stage === Stages.SendEmail ? ( + <> + + + + ) : stage === Stages.DeleteAccount ? ( + <> + + + + ) : null} + + + + + ) +}