forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import React from 'react'
2import {
3 ActivityIndicator,
4 SafeAreaView,
5 StyleSheet,
6 TouchableOpacity,
7 View,
8} from 'react-native'
9import {LinearGradient} from 'expo-linear-gradient'
10import {msg, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12
13import {DM_SERVICE_HEADERS} from '#/lib/constants'
14import {usePalette} from '#/lib/hooks/usePalette'
15import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
16import {cleanError} from '#/lib/strings/errors'
17import {colors, gradients, s} from '#/lib/styles'
18import {useTheme} from '#/lib/ThemeContext'
19import {isAndroid, isWeb} from '#/platform/detection'
20import {useModalControls} from '#/state/modals'
21import {useAgent, useSession, useSessionApi} from '#/state/session'
22import {atoms as a, useTheme as useNewTheme} from '#/alf'
23import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
24import {Text as NewText} from '#/components/Typography'
25import {resetToTab} from '../../../Navigation'
26import {ErrorMessage} from '../util/error/ErrorMessage'
27import {Text} from '../util/text/Text'
28import * as Toast from '../util/Toast'
29import {ScrollView, TextInput} from './util'
30
31export const snapPoints = isAndroid ? ['90%'] : ['55%']
32
33export function Component({}: {}) {
34 const pal = usePalette('default')
35 const theme = useTheme()
36 const t = useNewTheme()
37 const {currentAccount} = useSession()
38 const agent = useAgent()
39 const {removeAccount} = useSessionApi()
40 const {_} = useLingui()
41 const {closeModal} = useModalControls()
42 const {isMobile} = useWebMediaQueries()
43 const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
44 const [confirmCode, setConfirmCode] = React.useState<string>('')
45 const [password, setPassword] = React.useState<string>('')
46 const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
47 const [error, setError] = React.useState<string>('')
48 const onPressSendEmail = async () => {
49 setError('')
50 setIsProcessing(true)
51 try {
52 await agent.com.atproto.server.requestAccountDelete()
53 setIsEmailSent(true)
54 } catch (e: any) {
55 setError(cleanError(e))
56 }
57 setIsProcessing(false)
58 }
59 const onPressConfirmDelete = async () => {
60 if (!currentAccount?.did) {
61 throw new Error(`DeleteAccount modal: currentAccount.did is undefined`)
62 }
63
64 setError('')
65 setIsProcessing(true)
66 const token = confirmCode.replace(/\s/g, '')
67
68 try {
69 // inform chat service of intent to delete account
70 const {success} = await agent.api.chat.bsky.actor.deleteAccount(
71 undefined,
72 {
73 headers: DM_SERVICE_HEADERS,
74 },
75 )
76 if (!success) {
77 throw new Error('Failed to inform chat service of account deletion')
78 }
79 await agent.com.atproto.server.deleteAccount({
80 did: currentAccount.did,
81 password,
82 token,
83 })
84 Toast.show(_(msg`Your account has been deleted`))
85 resetToTab('HomeTab')
86 removeAccount(currentAccount)
87 closeModal()
88 } catch (e: any) {
89 setError(cleanError(e))
90 }
91 setIsProcessing(false)
92 }
93 const onCancel = () => {
94 closeModal()
95 }
96 return (
97 <SafeAreaView style={[s.flex1]}>
98 <ScrollView style={[pal.view]} keyboardShouldPersistTaps="handled">
99 <View style={[styles.titleContainer, pal.view]}>
100 <Text type="title-xl" style={[s.textCenter, pal.text]}>
101 <Trans>
102 Delete Account{' '}
103 <Text type="title-xl" style={[pal.text, s.bold]}>
104 "
105 </Text>
106 <Text
107 type="title-xl"
108 numberOfLines={1}
109 style={[
110 isMobile ? styles.titleMobile : styles.titleDesktop,
111 pal.text,
112 s.bold,
113 ]}>
114 {currentAccount?.handle}
115 </Text>
116 <Text type="title-xl" style={[pal.text, s.bold]}>
117 "
118 </Text>
119 </Trans>
120 </Text>
121 </View>
122 {!isEmailSent ? (
123 <>
124 <Text type="lg" style={[styles.description, pal.text]}>
125 <Trans>
126 For security reasons, we'll need to send a confirmation code to
127 your email address.
128 </Trans>
129 </Text>
130 {error ? (
131 <View style={s.mt10}>
132 <ErrorMessage message={error} />
133 </View>
134 ) : undefined}
135 {isProcessing ? (
136 <View style={[styles.btn, s.mt10]}>
137 <ActivityIndicator />
138 </View>
139 ) : (
140 <>
141 <TouchableOpacity
142 style={styles.mt20}
143 onPress={onPressSendEmail}
144 accessibilityRole="button"
145 accessibilityLabel={_(msg`Send email`)}
146 accessibilityHint={_(
147 msg`Sends email with confirmation code for account deletion`,
148 )}>
149 <LinearGradient
150 colors={[
151 gradients.blueLight.start,
152 gradients.blueLight.end,
153 ]}
154 start={{x: 0, y: 0}}
155 end={{x: 1, y: 1}}
156 style={[styles.btn]}>
157 <Text type="button-lg" style={[s.white, s.bold]}>
158 <Trans context="action">Send Email</Trans>
159 </Text>
160 </LinearGradient>
161 </TouchableOpacity>
162 <TouchableOpacity
163 style={[styles.btn, s.mt10]}
164 onPress={onCancel}
165 accessibilityRole="button"
166 accessibilityLabel={_(msg`Cancel account deletion`)}
167 accessibilityHint=""
168 onAccessibilityEscape={onCancel}>
169 <Text type="button-lg" style={pal.textLight}>
170 <Trans context="action">Cancel</Trans>
171 </Text>
172 </TouchableOpacity>
173 </>
174 )}
175
176 <View style={[!isWeb && a.px_xl]}>
177 <View
178 style={[
179 a.w_full,
180 a.flex_row,
181 a.gap_sm,
182 a.mt_lg,
183 a.p_lg,
184 a.rounded_sm,
185 t.atoms.bg_contrast_25,
186 ]}>
187 <CircleInfo
188 size="md"
189 style={[
190 a.relative,
191 {
192 top: -1,
193 },
194 ]}
195 />
196
197 <NewText style={[a.leading_snug, a.flex_1]}>
198 <Trans>
199 You can also temporarily deactivate your account instead,
200 and reactivate it at any time.
201 </Trans>
202 </NewText>
203 </View>
204 </View>
205 </>
206 ) : (
207 <>
208 {/* TODO: Update this label to be more concise */}
209 <Text
210 type="lg"
211 style={[pal.text, styles.description]}
212 nativeID="confirmationCode">
213 <Trans>
214 Check your inbox for an email with the confirmation code to
215 enter below:
216 </Trans>
217 </Text>
218 <TextInput
219 style={[styles.textInput, pal.borderDark, pal.text, styles.mb20]}
220 placeholder={_(msg`Confirmation code`)}
221 placeholderTextColor={pal.textLight.color}
222 keyboardAppearance={theme.colorScheme}
223 value={confirmCode}
224 onChangeText={setConfirmCode}
225 accessibilityLabelledBy="confirmationCode"
226 accessibilityLabel={_(msg`Confirmation code`)}
227 accessibilityHint={_(
228 msg`Input confirmation code for account deletion`,
229 )}
230 />
231 <Text
232 type="lg"
233 style={[pal.text, styles.description]}
234 nativeID="password">
235 <Trans>Please enter your password as well:</Trans>
236 </Text>
237 <TextInput
238 style={[styles.textInput, pal.borderDark, pal.text]}
239 placeholder={_(msg`Password`)}
240 placeholderTextColor={pal.textLight.color}
241 keyboardAppearance={theme.colorScheme}
242 secureTextEntry
243 value={password}
244 onChangeText={setPassword}
245 accessibilityLabelledBy="password"
246 accessibilityLabel={_(msg`Password`)}
247 accessibilityHint={_(msg`Input password for account deletion`)}
248 />
249 {error ? (
250 <View style={styles.mt20}>
251 <ErrorMessage message={error} />
252 </View>
253 ) : undefined}
254 {isProcessing ? (
255 <View style={[styles.btn, s.mt10]}>
256 <ActivityIndicator />
257 </View>
258 ) : (
259 <>
260 <TouchableOpacity
261 style={[styles.btn, styles.evilBtn, styles.mt20]}
262 onPress={onPressConfirmDelete}
263 accessibilityRole="button"
264 accessibilityLabel={_(msg`Confirm delete account`)}
265 accessibilityHint="">
266 <Text type="button-lg" style={[s.white, s.bold]}>
267 <Trans>Delete my account</Trans>
268 </Text>
269 </TouchableOpacity>
270 <TouchableOpacity
271 style={[styles.btn, s.mt10]}
272 onPress={onCancel}
273 accessibilityRole="button"
274 accessibilityLabel={_(msg`Cancel account deletion`)}
275 accessibilityHint={_(msg`Exits account deletion process`)}
276 onAccessibilityEscape={onCancel}>
277 <Text type="button-lg" style={pal.textLight}>
278 <Trans context="action">Cancel</Trans>
279 </Text>
280 </TouchableOpacity>
281 </>
282 )}
283 </>
284 )}
285 </ScrollView>
286 </SafeAreaView>
287 )
288}
289
290const styles = StyleSheet.create({
291 titleContainer: {
292 display: 'flex',
293 flexDirection: 'row',
294 justifyContent: 'center',
295 flexWrap: 'wrap',
296 marginTop: 12,
297 marginBottom: 12,
298 marginLeft: 20,
299 marginRight: 20,
300 },
301 titleMobile: {
302 textAlign: 'center',
303 },
304 titleDesktop: {
305 textAlign: 'center',
306 overflow: 'hidden',
307 whiteSpace: 'nowrap',
308 textOverflow: 'ellipsis',
309 // @ts-ignore only rendered on web
310 maxWidth: '400px',
311 },
312 description: {
313 textAlign: 'center',
314 paddingHorizontal: 22,
315 marginBottom: 10,
316 },
317 mt20: {
318 marginTop: 20,
319 },
320 mb20: {
321 marginBottom: 20,
322 },
323 textInput: {
324 borderWidth: 1,
325 borderRadius: 6,
326 paddingHorizontal: 16,
327 paddingVertical: 12,
328 fontSize: 20,
329 marginHorizontal: 20,
330 },
331 btn: {
332 flexDirection: 'row',
333 alignItems: 'center',
334 justifyContent: 'center',
335 borderRadius: 32,
336 padding: 14,
337 marginHorizontal: 20,
338 },
339 evilBtn: {
340 backgroundColor: colors.red4,
341 },
342})