new delete account dialog

This commit is contained in:
Samuel Newman
2025-04-22 16:21:20 +03:00
parent 4791486aa7
commit c864624543
3 changed files with 268 additions and 10 deletions
+7 -3
View File
@@ -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) {
</SettingsList.PressableItem>
<SettingsList.PressableItem
label={_(msg`Delete account`)}
onPress={() => openModal({name: 'delete-account'})}
onPress={() => deleteAccountControl.open()}
destructive>
<SettingsList.ItemIcon icon={Trash_Stroke2_Corner2_Rounded} />
<SettingsList.ItemText>
@@ -183,6 +183,10 @@ export function AccountSettingsScreen({}: Props) {
<BirthDateSettingsDialog control={birthdayControl} />
<ChangeHandleDialog control={changeHandleControl} />
<ChangePasswordDialog control={changePasswordControl} />
<DeleteAccountDialog
control={deleteAccountControl}
deactivateDialogControl={deactivateAccountControl}
/>
<ExportCarDialog control={exportCarControl} />
<DeactivateAccountDialog control={deactivateAccountControl} />
</Layout.Screen>
@@ -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 (
<Prompt.Outer control={control}>
@@ -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<string | undefined>()
const [pending, setPending] = useState(false)
const [error, setError] = useState<string | undefined>()
const handleDeactivate = React.useCallback(async () => {
const handleDeactivate = useCallback(async () => {
try {
setPending(true)
await agent.com.atproto.server.deactivateAccount({})
@@ -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 (
<Dialog.Outer control={control}>
<Dialog.Handle />
<Inner deactivateDialogControl={deactivateDialogControl} />
</Dialog.Outer>
)
}
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 (
<Dialog.ScrollableInner
label={_(msg`Delete account dialog`)}
style={web({maxWidth: 450})}>
<View style={[a.gap_xl]}>
<View style={[a.gap_sm]}>
<Text style={[a.font_heavy, a.text_2xl]}>{title}</Text>
{error ? (
<View style={[a.rounded_sm, a.overflow_hidden]}>
<ErrorMessage message={error} />
</View>
) : null}
<Text style={[a.text_md, a.leading_snug]}>
{stage === Stages.SendEmail ? (
<Trans>
For security reasons, we'll need to send a confirmation code to
your email address.
</Trans>
) : (
<Trans>
Check your inbox for an email with the confirmation code to
enter below:
</Trans>
)}
</Text>
</View>
{stage === Stages.SendEmail && (
<Admonition>
<Trans>
You can also{' '}
<RNText
style={[{color: t.palette.primary_500}, web(a.underline)]}
onPress={() =>
control.close(() => deactivateDialogControl.open())
}>
temporarily deactivate
</RNText>{' '}
your account instead, and reactivate it at any time.
</Trans>
</Admonition>
)}
{stage === Stages.DeleteAccount && (
<View style={[a.gap_md]}>
<View>
<TextField.LabelText>
<Trans>Confirmation Code</Trans>
</TextField.LabelText>
<TextField.Root>
<TextField.Input
label={_(msg`Confirmation code`)}
placeholder={_(msg`Enter code from email`)}
value={confirmCode}
onChangeText={setConfirmCode}
autoCapitalize="none"
autoCorrect={false}
autoComplete="one-time-code"
/>
</TextField.Root>
</View>
<View>
<TextField.LabelText>
<Trans>Password</Trans>
</TextField.LabelText>
<TextField.Root>
<TextField.Input
label={_(msg`Password`)}
placeholder={_(msg`Enter your password`)}
value={password}
onChangeText={setPassword}
secureTextEntry
autoCapitalize="none"
autoComplete="password"
/>
</TextField.Root>
</View>
</View>
)}
<View style={[a.gap_sm, gtMobile && [a.flex_row_reverse, a.ml_auto]]}>
{stage === Stages.SendEmail ? (
<>
<Button
label={_(msg`Send Email`)}
variant="solid"
color="negative"
size="large"
disabled={isProcessing}
onPress={onPressSendEmail}>
<ButtonText>
<Trans>Send Email</Trans>
</ButtonText>
{isProcessing && <ButtonIcon icon={Loader} />}
</Button>
<Button
label={_(msg`Cancel`)}
variant="solid"
color="secondary"
size="large"
disabled={isProcessing}
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
</>
) : stage === Stages.DeleteAccount ? (
<>
<Button
label={_(msg`Delete account`)}
variant="solid"
color="negative"
size="large"
disabled={isProcessing}
onPress={onPressConfirmDelete}>
<ButtonText>
<Trans>Delete Account</Trans>
</ButtonText>
{isProcessing && <ButtonIcon icon={Loader} />}
</Button>
<Button
label={_(msg`Cancel`)}
variant="solid"
color="secondary"
size="large"
disabled={isProcessing}
onPress={() => control.close()}>
<ButtonText>
<Trans>Cancel</Trans>
</ButtonText>
</Button>
</>
) : null}
</View>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}