Refactor delete account dialog using ALF (#9863)
This commit is contained in:
@@ -32,8 +32,11 @@ export function TokenField({
|
|||||||
<TextField.Root>
|
<TextField.Root>
|
||||||
<TextField.Icon icon={Shield} />
|
<TextField.Icon icon={Shield} />
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect={false}
|
||||||
isInvalid={isInvalid}
|
isInvalid={isInvalid}
|
||||||
label={_(msg`Confirmation code`)}
|
label={_(msg`Confirmation code`)}
|
||||||
|
maxLength={11}
|
||||||
placeholder="XXXXX-XXXXX"
|
placeholder="XXXXX-XXXXX"
|
||||||
value={value}
|
value={value}
|
||||||
onChangeText={handleOnChangeText}
|
onChangeText={handleOnChangeText}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
import {type CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
import * as SettingsList from '#/screens/Settings/components/SettingsList'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -27,6 +26,7 @@ import * as Layout from '#/components/Layout'
|
|||||||
import {ChangeHandleDialog} from './components/ChangeHandleDialog'
|
import {ChangeHandleDialog} from './components/ChangeHandleDialog'
|
||||||
import {ChangePasswordDialog} from './components/ChangePasswordDialog'
|
import {ChangePasswordDialog} from './components/ChangePasswordDialog'
|
||||||
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
|
import {DeactivateAccountDialog} from './components/DeactivateAccountDialog'
|
||||||
|
import {DeleteAccountDialog} from './components/DeleteAccountDialog'
|
||||||
import {ExportCarDialog} from './components/ExportCarDialog'
|
import {ExportCarDialog} from './components/ExportCarDialog'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'AccountSettings'>
|
||||||
@@ -34,13 +34,13 @@ export function AccountSettingsScreen({}: Props) {
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const {openModal} = useModalControls()
|
|
||||||
const emailDialogControl = useEmailDialogControl()
|
const emailDialogControl = useEmailDialogControl()
|
||||||
const birthdayControl = useDialogControl()
|
const birthdayControl = useDialogControl()
|
||||||
const changeHandleControl = useDialogControl()
|
const changeHandleControl = useDialogControl()
|
||||||
const changePasswordControl = useDialogControl()
|
const changePasswordControl = useDialogControl()
|
||||||
const exportCarControl = useDialogControl()
|
const exportCarControl = useDialogControl()
|
||||||
const deactivateAccountControl = useDialogControl()
|
const deactivateAccountControl = useDialogControl()
|
||||||
|
const deleteAccountControl = useDialogControl()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -169,7 +169,7 @@ export function AccountSettingsScreen({}: Props) {
|
|||||||
</SettingsList.PressableItem>
|
</SettingsList.PressableItem>
|
||||||
<SettingsList.PressableItem
|
<SettingsList.PressableItem
|
||||||
label={_(msg`Delete account`)}
|
label={_(msg`Delete account`)}
|
||||||
onPress={() => openModal({name: 'delete-account'})}
|
onPress={() => deleteAccountControl.open()}
|
||||||
destructive>
|
destructive>
|
||||||
<SettingsList.ItemIcon icon={Trash_Stroke2_Corner2_Rounded} />
|
<SettingsList.ItemIcon icon={Trash_Stroke2_Corner2_Rounded} />
|
||||||
<SettingsList.ItemText>
|
<SettingsList.ItemText>
|
||||||
@@ -185,6 +185,10 @@ export function AccountSettingsScreen({}: Props) {
|
|||||||
<ChangePasswordDialog control={changePasswordControl} />
|
<ChangePasswordDialog control={changePasswordControl} />
|
||||||
<ExportCarDialog control={exportCarControl} />
|
<ExportCarDialog control={exportCarControl} />
|
||||||
<DeactivateAccountDialog control={deactivateAccountControl} />
|
<DeactivateAccountDialog control={deactivateAccountControl} />
|
||||||
|
<DeleteAccountDialog
|
||||||
|
control={deleteAccountControl}
|
||||||
|
deactivateDialogControl={deactivateAccountControl}
|
||||||
|
/>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,371 @@
|
|||||||
|
import {useCallback, useRef, useState} from 'react'
|
||||||
|
import {type TextInput, View} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
|
import {useCleanError} from '#/lib/hooks/useCleanError'
|
||||||
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {useAgent, useSession, useSessionApi} from '#/state/session'
|
||||||
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
|
import {Admonition} from '#/components/Admonition'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import {type DialogOuterProps} from '#/components/Dialog'
|
||||||
|
import {
|
||||||
|
isValidCode,
|
||||||
|
TokenField,
|
||||||
|
} from '#/components/dialogs/EmailDialog/components/TokenField'
|
||||||
|
import * as TextField from '#/components/forms/TextField'
|
||||||
|
import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/Envelope'
|
||||||
|
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
|
||||||
|
import {createStaticClick, InlineLinkText} from '#/components/Link'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
|
import * as Prompt from '#/components/Prompt'
|
||||||
|
import * as toast from '#/components/Toast'
|
||||||
|
import {Span, Text} from '#/components/Typography'
|
||||||
|
import {resetToTab} from '#/Navigation'
|
||||||
|
|
||||||
|
const WHITESPACE_RE = /\s/gu
|
||||||
|
const PASSWORD_MIN_LENGTH = 8
|
||||||
|
|
||||||
|
enum Step {
|
||||||
|
SEND_CODE,
|
||||||
|
VERIFY_CODE,
|
||||||
|
CONFIRM_DELETION,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EmailState {
|
||||||
|
DEFAULT,
|
||||||
|
PENDING,
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPasswordValid(password: string) {
|
||||||
|
return password.length >= PASSWORD_MIN_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteAccountDialog({
|
||||||
|
control,
|
||||||
|
deactivateDialogControl,
|
||||||
|
}: {
|
||||||
|
control: DialogOuterProps['control']
|
||||||
|
deactivateDialogControl: DialogOuterProps['control']
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Prompt.Outer control={control}>
|
||||||
|
<DeleteAccountDialogInner
|
||||||
|
control={control}
|
||||||
|
deactivateDialogControl={deactivateDialogControl}
|
||||||
|
/>
|
||||||
|
</Prompt.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeleteAccountDialogInner({
|
||||||
|
control,
|
||||||
|
deactivateDialogControl,
|
||||||
|
}: {
|
||||||
|
control: DialogOuterProps['control']
|
||||||
|
deactivateDialogControl: DialogOuterProps['control']
|
||||||
|
}) {
|
||||||
|
const passwordRef = useRef<TextInput | null>(null)
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const cleanError = useCleanError()
|
||||||
|
const agent = useAgent()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const {removeAccount} = useSessionApi()
|
||||||
|
|
||||||
|
const [emailState, setEmailState] = useState(EmailState.DEFAULT)
|
||||||
|
const [emailSentCount, setEmailSentCount] = useState(0)
|
||||||
|
const [step, setStep] = useState(Step.SEND_CODE)
|
||||||
|
const [confirmCode, setConfirmCode] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
const sendEmail = useCallback(async () => {
|
||||||
|
if (emailState === EmailState.PENDING) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setEmailState(EmailState.PENDING)
|
||||||
|
await agent.com.atproto.server.requestAccountDelete()
|
||||||
|
setError('')
|
||||||
|
setEmailSentCount(prevCount => prevCount + 1)
|
||||||
|
setStep(Step.VERIFY_CODE)
|
||||||
|
} catch (e: any) {
|
||||||
|
const {clean, raw} = cleanError(e)
|
||||||
|
const error = clean || raw || e
|
||||||
|
setError(error)
|
||||||
|
logger.error(raw || e, {
|
||||||
|
message: 'Failed to send account deletion verification email',
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setEmailState(EmailState.DEFAULT)
|
||||||
|
}
|
||||||
|
}, [agent, cleanError, emailState, setEmailState])
|
||||||
|
|
||||||
|
const confirmDeletion = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setError('')
|
||||||
|
if (!currentAccount?.did) {
|
||||||
|
throw new Error('Invalid did')
|
||||||
|
}
|
||||||
|
const token = confirmCode.replace(WHITESPACE_RE, '')
|
||||||
|
// 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,
|
||||||
|
})
|
||||||
|
control.close(() => {
|
||||||
|
toast.show(_(msg`Your account has been deleted, see ya! ✌️`))
|
||||||
|
resetToTab('HomeTab')
|
||||||
|
removeAccount(currentAccount)
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
const {clean, raw} = cleanError(e)
|
||||||
|
const error = clean || raw || e
|
||||||
|
setError(error)
|
||||||
|
logger.error(raw || e, {
|
||||||
|
message: 'Failed to delete account',
|
||||||
|
})
|
||||||
|
setConfirmCode('')
|
||||||
|
setPassword('')
|
||||||
|
setStep(Step.VERIFY_CODE)
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
_,
|
||||||
|
agent,
|
||||||
|
cleanError,
|
||||||
|
confirmCode,
|
||||||
|
control,
|
||||||
|
currentAccount,
|
||||||
|
password,
|
||||||
|
removeAccount,
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleDeactivate = useCallback(() => {
|
||||||
|
control.close(() => deactivateDialogControl.open())
|
||||||
|
}, [control, deactivateDialogControl])
|
||||||
|
|
||||||
|
const handleSendEmail = useCallback(() => {
|
||||||
|
void sendEmail()
|
||||||
|
}, [sendEmail])
|
||||||
|
|
||||||
|
const handleSubmitConfirmCode = useCallback(() => {
|
||||||
|
passwordRef.current?.focus()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleDeleteAccount = useCallback(() => {
|
||||||
|
setStep(Step.CONFIRM_DELETION)
|
||||||
|
}, [setStep])
|
||||||
|
|
||||||
|
const handleConfirmDeletion = useCallback(() => {
|
||||||
|
void confirmDeletion()
|
||||||
|
}, [confirmDeletion])
|
||||||
|
|
||||||
|
const currentHandle = sanitizeHandle(currentAccount?.handle ?? '', '@')
|
||||||
|
const currentEmail = currentAccount?.email ?? '(no email)'
|
||||||
|
|
||||||
|
switch (step) {
|
||||||
|
case Step.SEND_CODE:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Prompt.Content>
|
||||||
|
<Prompt.TitleText>
|
||||||
|
{_(msg`Delete account “${currentHandle}”`)}
|
||||||
|
</Prompt.TitleText>
|
||||||
|
<Prompt.DescriptionText>
|
||||||
|
<Trans>
|
||||||
|
For security reasons, we’ll need to send a confirmation code to
|
||||||
|
your email address{' '}
|
||||||
|
<Span style={[a.font_semi_bold, t.atoms.text]}>
|
||||||
|
{currentEmail}
|
||||||
|
</Span>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</Prompt.DescriptionText>
|
||||||
|
</Prompt.Content>
|
||||||
|
<Prompt.Actions>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
label={_(msg`Send email`)}
|
||||||
|
size="large"
|
||||||
|
onPress={handleSendEmail}>
|
||||||
|
<ButtonText>{_(msg`Send email`)}</ButtonText>
|
||||||
|
<ButtonIcon
|
||||||
|
icon={emailState === EmailState.PENDING ? Loader : Envelope}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<Prompt.Cancel />
|
||||||
|
</Prompt.Actions>
|
||||||
|
{error && (
|
||||||
|
<Admonition style={[a.mt_lg]} type="error">
|
||||||
|
<Text style={[a.flex_1, a.leading_snug]}>{error}</Text>
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
<Admonition style={[a.mt_lg]} type="tip">
|
||||||
|
<Trans>
|
||||||
|
You can also{' '}
|
||||||
|
<Span
|
||||||
|
style={[{color: t.palette.primary_500}, web(a.underline)]}
|
||||||
|
onPress={handleDeactivate}>
|
||||||
|
temporarily deactivate
|
||||||
|
</Span>{' '}
|
||||||
|
your account instead. Your profile, posts, feeds, and lists will
|
||||||
|
no longer be visible to other Bluesky users. You can reactivate
|
||||||
|
your account at any time by logging in.
|
||||||
|
</Trans>
|
||||||
|
</Admonition>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case Step.VERIFY_CODE:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Prompt.Content>
|
||||||
|
<Prompt.TitleText>
|
||||||
|
{_(msg`Delete account “${currentHandle}”`)}
|
||||||
|
</Prompt.TitleText>
|
||||||
|
<Prompt.DescriptionText>
|
||||||
|
<Trans>
|
||||||
|
Check{' '}
|
||||||
|
<Span style={[a.font_semi_bold, t.atoms.text]}>
|
||||||
|
{currentEmail}
|
||||||
|
</Span>{' '}
|
||||||
|
for an email with the confirmation code to enter below:
|
||||||
|
</Trans>
|
||||||
|
</Prompt.DescriptionText>
|
||||||
|
</Prompt.Content>
|
||||||
|
<View style={[a.mb_xs]}>
|
||||||
|
<TextField.LabelText>
|
||||||
|
<Trans>Confirmation code</Trans>
|
||||||
|
</TextField.LabelText>
|
||||||
|
<TokenField
|
||||||
|
value={confirmCode}
|
||||||
|
onChangeText={setConfirmCode}
|
||||||
|
onSubmitEditing={handleSubmitConfirmCode}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_sm,
|
||||||
|
a.leading_snug,
|
||||||
|
a.mb_lg,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
]}>
|
||||||
|
{emailSentCount > 1 ? (
|
||||||
|
<Trans>
|
||||||
|
Email sent!{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
label={_(msg`Resend`)}
|
||||||
|
{...createStaticClick(() => {
|
||||||
|
void handleSendEmail()
|
||||||
|
})}>
|
||||||
|
Click here to resend.
|
||||||
|
</InlineLinkText>
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
Don’t see a code?{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
label={_(msg`Resend`)}
|
||||||
|
{...createStaticClick(() => {
|
||||||
|
void handleSendEmail()
|
||||||
|
})}>
|
||||||
|
Click here to resend.
|
||||||
|
</InlineLinkText>
|
||||||
|
</Trans>
|
||||||
|
)}{' '}
|
||||||
|
<Span style={{top: 1}}>
|
||||||
|
{emailState === EmailState.PENDING ? <Loader size="xs" /> : null}
|
||||||
|
</Span>
|
||||||
|
</Text>
|
||||||
|
<View style={[a.mb_xl]}>
|
||||||
|
<TextField.LabelText>
|
||||||
|
<Trans>Password</Trans>
|
||||||
|
</TextField.LabelText>
|
||||||
|
<TextField.Root>
|
||||||
|
<TextField.Icon icon={Lock} />
|
||||||
|
<TextField.Input
|
||||||
|
inputRef={passwordRef}
|
||||||
|
testID="newPasswordInput"
|
||||||
|
label={_(msg`Enter your password`)}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
returnKeyType="done"
|
||||||
|
secureTextEntry={true}
|
||||||
|
autoComplete="off"
|
||||||
|
clearButtonMode="while-editing"
|
||||||
|
passwordRules={`minlength: ${PASSWORD_MIN_LENGTH}};`}
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
onSubmitEditing={handleDeleteAccount}
|
||||||
|
/>
|
||||||
|
</TextField.Root>
|
||||||
|
</View>
|
||||||
|
<Prompt.Actions>
|
||||||
|
<Button
|
||||||
|
color="negative"
|
||||||
|
disabled={!isValidCode(confirmCode) || !isPasswordValid(password)}
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Delete My Account`)}
|
||||||
|
onPress={handleDeleteAccount}>
|
||||||
|
<ButtonText>{_(msg`Delete My Account`)}</ButtonText>
|
||||||
|
</Button>
|
||||||
|
<Prompt.Cancel />
|
||||||
|
</Prompt.Actions>
|
||||||
|
{error && (
|
||||||
|
<Admonition style={[a.mt_lg]} type="error">
|
||||||
|
<Text style={[a.flex_1, a.leading_snug]}>{error}</Text>
|
||||||
|
</Admonition>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
case Step.CONFIRM_DELETION:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Prompt.Content>
|
||||||
|
<Prompt.TitleText>
|
||||||
|
{_(msg`Are you really, really sure?`)}
|
||||||
|
</Prompt.TitleText>
|
||||||
|
<Prompt.DescriptionText>
|
||||||
|
<Trans>
|
||||||
|
This will irreversably delete your Bluesky account{' '}
|
||||||
|
<Span style={[a.font_semi_bold, t.atoms.text]}>
|
||||||
|
{currentHandle}
|
||||||
|
</Span>{' '}
|
||||||
|
and all associated data. Note that this will affect any other{' '}
|
||||||
|
<InlineLinkText
|
||||||
|
label={_(msg`Learn more about the AT Protocol.`)}
|
||||||
|
style={[a.text_md]}
|
||||||
|
to="https://bsky.social/about/faq">
|
||||||
|
AT Protocol
|
||||||
|
</InlineLinkText>{' '}
|
||||||
|
services you use with this account.
|
||||||
|
</Trans>
|
||||||
|
</Prompt.DescriptionText>
|
||||||
|
</Prompt.Content>
|
||||||
|
<Prompt.Actions>
|
||||||
|
<Button
|
||||||
|
color="negative"
|
||||||
|
size="large"
|
||||||
|
label={_(msg`Yes, delete my account`)}
|
||||||
|
onPress={handleConfirmDeletion}>
|
||||||
|
<ButtonText>{_(msg`Yes, delete my account`)}</ButtonText>
|
||||||
|
</Button>
|
||||||
|
<Prompt.Cancel />
|
||||||
|
</Prompt.Actions>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,6 @@ export interface UserAddRemoveListsModal {
|
|||||||
onRemove?: (listUri: string) => void
|
onRemove?: (listUri: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteAccountModal {
|
|
||||||
name: 'delete-account'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContentLanguagesSettingsModal {
|
export interface ContentLanguagesSettingsModal {
|
||||||
name: 'content-languages-settings'
|
name: 'content-languages-settings'
|
||||||
}
|
}
|
||||||
@@ -23,9 +19,6 @@ export interface ContentLanguagesSettingsModal {
|
|||||||
* @deprecated DO NOT ADD NEW MODALS
|
* @deprecated DO NOT ADD NEW MODALS
|
||||||
*/
|
*/
|
||||||
export type Modal =
|
export type Modal =
|
||||||
// Account
|
|
||||||
| DeleteAccountModal
|
|
||||||
|
|
||||||
// Curation
|
// Curation
|
||||||
| ContentLanguagesSettingsModal
|
| ContentLanguagesSettingsModal
|
||||||
|
|
||||||
|
|||||||
@@ -1,342 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
SafeAreaView,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
|
||||||
import {colors, gradients, s} from '#/lib/styles'
|
|
||||||
import {useTheme} from '#/lib/ThemeContext'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {useAgent, useSession, useSessionApi} from '#/state/session'
|
|
||||||
import {atoms as a, useTheme as useNewTheme} from '#/alf'
|
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
|
||||||
import {Text as NewText} from '#/components/Typography'
|
|
||||||
import {IS_ANDROID, IS_WEB} from '#/env'
|
|
||||||
import {resetToTab} from '../../../Navigation'
|
|
||||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
|
||||||
import {Text} from '../util/text/Text'
|
|
||||||
import * as Toast from '../util/Toast'
|
|
||||||
import {ScrollView, TextInput} from './util'
|
|
||||||
|
|
||||||
export const snapPoints = IS_ANDROID ? ['90%'] : ['55%']
|
|
||||||
|
|
||||||
export function Component({}: {}) {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
|
||||||
const t = useNewTheme()
|
|
||||||
const {currentAccount} = useSession()
|
|
||||||
const agent = useAgent()
|
|
||||||
const {removeAccount} = useSessionApi()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {closeModal} = useModalControls()
|
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
|
|
||||||
const [confirmCode, setConfirmCode] = React.useState<string>('')
|
|
||||||
const [password, setPassword] = React.useState<string>('')
|
|
||||||
const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
|
|
||||||
const [error, setError] = React.useState<string>('')
|
|
||||||
const onPressSendEmail = async () => {
|
|
||||||
setError('')
|
|
||||||
setIsProcessing(true)
|
|
||||||
try {
|
|
||||||
await agent.com.atproto.server.requestAccountDelete()
|
|
||||||
setIsEmailSent(true)
|
|
||||||
} 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)
|
|
||||||
closeModal()
|
|
||||||
} catch (e: any) {
|
|
||||||
setError(cleanError(e))
|
|
||||||
}
|
|
||||||
setIsProcessing(false)
|
|
||||||
}
|
|
||||||
const onCancel = () => {
|
|
||||||
closeModal()
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={[s.flex1]}>
|
|
||||||
<ScrollView style={[pal.view]} keyboardShouldPersistTaps="handled">
|
|
||||||
<View style={[styles.titleContainer, pal.view]}>
|
|
||||||
<Text type="title-xl" style={[s.textCenter, pal.text]}>
|
|
||||||
<Trans>
|
|
||||||
Delete Account{' '}
|
|
||||||
<Text type="title-xl" style={[pal.text, s.bold]}>
|
|
||||||
"
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
type="title-xl"
|
|
||||||
numberOfLines={1}
|
|
||||||
style={[
|
|
||||||
isMobile ? styles.titleMobile : styles.titleDesktop,
|
|
||||||
pal.text,
|
|
||||||
s.bold,
|
|
||||||
]}>
|
|
||||||
{currentAccount?.handle}
|
|
||||||
</Text>
|
|
||||||
<Text type="title-xl" style={[pal.text, s.bold]}>
|
|
||||||
"
|
|
||||||
</Text>
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
{!isEmailSent ? (
|
|
||||||
<>
|
|
||||||
<Text type="lg" style={[styles.description, pal.text]}>
|
|
||||||
<Trans>
|
|
||||||
For security reasons, we'll need to send a confirmation code to
|
|
||||||
your email address.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
{error ? (
|
|
||||||
<View style={s.mt10}>
|
|
||||||
<ErrorMessage message={error} />
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
{isProcessing ? (
|
|
||||||
<View style={[styles.btn, s.mt10]}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.mt20}
|
|
||||||
onPress={onPressSendEmail}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Send email`)}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Sends email with confirmation code for account deletion`,
|
|
||||||
)}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[
|
|
||||||
gradients.blueLight.start,
|
|
||||||
gradients.blueLight.end,
|
|
||||||
]}
|
|
||||||
start={{x: 0, y: 0}}
|
|
||||||
end={{x: 1, y: 1}}
|
|
||||||
style={[styles.btn]}>
|
|
||||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
|
||||||
<Trans context="action">Send Email</Trans>
|
|
||||||
</Text>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.btn, s.mt10]}
|
|
||||||
onPress={onCancel}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Cancel account deletion`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
onAccessibilityEscape={onCancel}>
|
|
||||||
<Text type="button-lg" style={pal.textLight}>
|
|
||||||
<Trans context="action">Cancel</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View style={[!IS_WEB && a.px_xl]}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.flex_row,
|
|
||||||
a.gap_sm,
|
|
||||||
a.mt_lg,
|
|
||||||
a.p_lg,
|
|
||||||
a.rounded_sm,
|
|
||||||
t.atoms.bg_contrast_25,
|
|
||||||
]}>
|
|
||||||
<CircleInfo
|
|
||||||
size="md"
|
|
||||||
style={[
|
|
||||||
a.relative,
|
|
||||||
{
|
|
||||||
top: -1,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<NewText style={[a.leading_snug, a.flex_1]}>
|
|
||||||
<Trans>
|
|
||||||
You can also temporarily deactivate your account instead,
|
|
||||||
and reactivate it at any time.
|
|
||||||
</Trans>
|
|
||||||
</NewText>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* TODO: Update this label to be more concise */}
|
|
||||||
<Text
|
|
||||||
type="lg"
|
|
||||||
style={[pal.text, styles.description]}
|
|
||||||
nativeID="confirmationCode">
|
|
||||||
<Trans>
|
|
||||||
Check your inbox for an email with the confirmation code to
|
|
||||||
enter below:
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
|
|
||||||
placeholder={_(msg`Confirmation code`)}
|
|
||||||
placeholderTextColor={pal.textLight.color}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
value={confirmCode}
|
|
||||||
onChangeText={setConfirmCode}
|
|
||||||
accessibilityLabelledBy="confirmationCode"
|
|
||||||
accessibilityLabel={_(msg`Confirmation code`)}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Input confirmation code for account deletion`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
type="lg"
|
|
||||||
style={[pal.text, styles.description]}
|
|
||||||
nativeID="password">
|
|
||||||
<Trans>Please enter your password as well:</Trans>
|
|
||||||
</Text>
|
|
||||||
<TextInput
|
|
||||||
style={[styles.textInput, pal.borderDark, pal.text]}
|
|
||||||
placeholder={_(msg`Password`)}
|
|
||||||
placeholderTextColor={pal.textLight.color}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
secureTextEntry
|
|
||||||
value={password}
|
|
||||||
onChangeText={setPassword}
|
|
||||||
accessibilityLabelledBy="password"
|
|
||||||
accessibilityLabel={_(msg`Password`)}
|
|
||||||
accessibilityHint={_(msg`Input password for account deletion`)}
|
|
||||||
/>
|
|
||||||
{error ? (
|
|
||||||
<View style={styles.mt20}>
|
|
||||||
<ErrorMessage message={error} />
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
{isProcessing ? (
|
|
||||||
<View style={[styles.btn, s.mt10]}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.btn, styles.evilBtn, styles.mt20]}
|
|
||||||
onPress={onPressConfirmDelete}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Confirm delete account`)}
|
|
||||||
accessibilityHint="">
|
|
||||||
<Text type="button-lg" style={[s.white, s.bold]}>
|
|
||||||
<Trans>Delete my account</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.btn, s.mt10]}
|
|
||||||
onPress={onCancel}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Cancel account deletion`)}
|
|
||||||
accessibilityHint={_(msg`Exits account deletion process`)}
|
|
||||||
onAccessibilityEscape={onCancel}>
|
|
||||||
<Text type="button-lg" style={pal.textLight}>
|
|
||||||
<Trans context="action">Cancel</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</SafeAreaView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
titleContainer: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
marginTop: 12,
|
|
||||||
marginBottom: 12,
|
|
||||||
marginLeft: 20,
|
|
||||||
marginRight: 20,
|
|
||||||
},
|
|
||||||
titleMobile: {
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
titleDesktop: {
|
|
||||||
textAlign: 'center',
|
|
||||||
overflow: 'hidden',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
// @ts-ignore only rendered on web
|
|
||||||
maxWidth: '400px',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
textAlign: 'center',
|
|
||||||
paddingHorizontal: 22,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
mt20: {
|
|
||||||
marginTop: 20,
|
|
||||||
},
|
|
||||||
mb20: {
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
textInput: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 6,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingVertical: 12,
|
|
||||||
fontSize: 20,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
borderRadius: 32,
|
|
||||||
padding: 14,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
evilBtn: {
|
|
||||||
backgroundColor: colors.red4,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -7,7 +7,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
|
|||||||
import {useModalControls, useModals} from '#/state/modals'
|
import {useModalControls, useModals} from '#/state/modals'
|
||||||
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
||||||
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
|
import {createCustomBackdrop} from '../util/BottomSheetCustomBackdrop'
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
|
||||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
||||||
|
|
||||||
@@ -45,9 +44,6 @@ export function ModalsContainer() {
|
|||||||
if (activeModal?.name === 'user-add-remove-lists') {
|
if (activeModal?.name === 'user-add-remove-lists') {
|
||||||
snapPoints = UserAddRemoveListsModal.snapPoints
|
snapPoints = UserAddRemoveListsModal.snapPoints
|
||||||
element = <UserAddRemoveListsModal.Component {...activeModal} />
|
element = <UserAddRemoveListsModal.Component {...activeModal} />
|
||||||
} else if (activeModal?.name === 'delete-account') {
|
|
||||||
snapPoints = DeleteAccountModal.snapPoints
|
|
||||||
element = <DeleteAccountModal.Component />
|
|
||||||
} else if (activeModal?.name === 'content-languages-settings') {
|
} else if (activeModal?.name === 'content-languages-settings') {
|
||||||
snapPoints = ContentLanguagesSettingsModal.snapPoints
|
snapPoints = ContentLanguagesSettingsModal.snapPoints
|
||||||
element = <ContentLanguagesSettingsModal.Component />
|
element = <ContentLanguagesSettingsModal.Component />
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {usePalette} from '#/lib/hooks/usePalette'
|
|||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {type Modal as ModalIface} from '#/state/modals'
|
import {type Modal as ModalIface} from '#/state/modals'
|
||||||
import {useModalControls, useModals} from '#/state/modals'
|
import {useModalControls, useModals} from '#/state/modals'
|
||||||
import * as DeleteAccountModal from './DeleteAccount'
|
|
||||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||||
|
|
||||||
@@ -48,8 +47,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
let element
|
let element
|
||||||
if (modal.name === 'user-add-remove-lists') {
|
if (modal.name === 'user-add-remove-lists') {
|
||||||
element = <UserAddRemoveLists.Component {...modal} />
|
element = <UserAddRemoveLists.Component {...modal} />
|
||||||
} else if (modal.name === 'delete-account') {
|
|
||||||
element = <DeleteAccountModal.Component />
|
|
||||||
} else if (modal.name === 'content-languages-settings') {
|
} else if (modal.name === 'content-languages-settings') {
|
||||||
element = <ContentLanguagesSettingsModal.Component />
|
element = <ContentLanguagesSettingsModal.Component />
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user