forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useState} from 'react'
2import {useWindowDimensions, View} from 'react-native'
3import {msg, Trans} from '@lingui/macro'
4import {useLingui} from '@lingui/react'
5import * as EmailValidator from 'email-validator'
6
7import {cleanError, isNetworkError} from '#/lib/strings/errors'
8import {checkAndFormatResetCode} from '#/lib/strings/password'
9import {logger} from '#/logger'
10import {useAgent, useSession} from '#/state/session'
11import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
12import {android, atoms as a, web} from '#/alf'
13import {Button, ButtonIcon, ButtonText} from '#/components/Button'
14import * as Dialog from '#/components/Dialog'
15import * as TextField from '#/components/forms/TextField'
16import {Loader} from '#/components/Loader'
17import {Text} from '#/components/Typography'
18import {IS_NATIVE} from '#/env'
19
20enum Stages {
21 RequestCode = 'RequestCode',
22 ChangePassword = 'ChangePassword',
23 Done = 'Done',
24}
25
26export function ChangePasswordDialog({
27 control,
28}: {
29 control: Dialog.DialogControlProps
30}) {
31 const {height} = useWindowDimensions()
32
33 return (
34 <Dialog.Outer
35 control={control}
36 nativeOptions={android({minHeight: height / 2})}>
37 <Dialog.Handle />
38 <Inner />
39 </Dialog.Outer>
40 )
41}
42
43function Inner() {
44 const {_} = useLingui()
45 const {currentAccount} = useSession()
46 const agent = useAgent()
47 const control = Dialog.useDialogContext()
48
49 const [stage, setStage] = useState(Stages.RequestCode)
50 const [isProcessing, setIsProcessing] = useState(false)
51 const [resetCode, setResetCode] = useState('')
52 const [newPassword, setNewPassword] = useState('')
53 const [error, setError] = useState('')
54
55 const uiStrings = {
56 RequestCode: {
57 title: _(msg`Change your password`),
58 message: _(
59 msg`If you want to change your password, we will send you a code to verify that this is your account.`,
60 ),
61 },
62 ChangePassword: {
63 title: _(msg`Enter code`),
64 message: _(
65 msg`Please enter the code you received and the new password you would like to use.`,
66 ),
67 },
68 Done: {
69 title: _(msg`Password changed`),
70 message: _(
71 msg`Your password has been changed successfully! Please use your new password when you sign in to Bluesky from now on.`,
72 ),
73 },
74 }
75
76 const onRequestCode = async () => {
77 if (
78 !currentAccount?.email ||
79 !EmailValidator.validate(currentAccount.email)
80 ) {
81 return setError(_(msg`Your email appears to be invalid.`))
82 }
83
84 setError('')
85 setIsProcessing(true)
86 try {
87 await agent.com.atproto.server.requestPasswordReset({
88 email: currentAccount.email,
89 })
90 setStage(Stages.ChangePassword)
91 } catch (e: any) {
92 if (isNetworkError(e)) {
93 setError(
94 _(
95 msg`Unable to contact your service. Please check your internet connection and try again.`,
96 ),
97 )
98 } else {
99 logger.error('Failed to request password reset', {safeMessage: e})
100 setError(cleanError(e))
101 }
102 } finally {
103 setIsProcessing(false)
104 }
105 }
106
107 const onChangePassword = async () => {
108 const formattedCode = checkAndFormatResetCode(resetCode)
109 if (!formattedCode) {
110 setError(
111 _(
112 msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
113 ),
114 )
115 return
116 }
117 if (!newPassword) {
118 setError(
119 _(msg`Please enter a password. It must be at least 8 characters long.`),
120 )
121 return
122 }
123 if (newPassword.length < 8) {
124 setError(_(msg`Password must be at least 8 characters long.`))
125 return
126 }
127
128 setError('')
129 setIsProcessing(true)
130 try {
131 await agent.com.atproto.server.resetPassword({
132 token: formattedCode,
133 password: newPassword,
134 })
135 setStage(Stages.Done)
136 } catch (e: any) {
137 if (isNetworkError(e)) {
138 setError(
139 _(
140 msg`Unable to contact your service. Please check your internet connection and try again.`,
141 ),
142 )
143 } else if (e?.toString().includes('Token is invalid')) {
144 setError(_(msg`This confirmation code is not valid. Please try again.`))
145 } else {
146 logger.error('Failed to set new password', {safeMessage: e})
147 setError(cleanError(e))
148 }
149 } finally {
150 setIsProcessing(false)
151 }
152 }
153
154 const onBlur = () => {
155 const formattedCode = checkAndFormatResetCode(resetCode)
156 if (!formattedCode) {
157 return
158 }
159 setResetCode(formattedCode)
160 }
161
162 return (
163 <Dialog.ScrollableInner
164 label={_(msg`Change password dialog`)}
165 style={web({maxWidth: 400})}>
166 <View style={[a.gap_xl]}>
167 <View style={[a.gap_sm]}>
168 <Text style={[a.font_bold, a.text_2xl]}>
169 {uiStrings[stage].title}
170 </Text>
171 {error ? (
172 <View style={[a.rounded_sm, a.overflow_hidden]}>
173 <ErrorMessage message={error} />
174 </View>
175 ) : null}
176
177 <Text style={[a.text_md, a.leading_snug]}>
178 {uiStrings[stage].message}
179 </Text>
180 </View>
181
182 {stage === Stages.ChangePassword && (
183 <View style={[a.gap_md]}>
184 <View>
185 <TextField.LabelText>
186 <Trans>Confirmation code</Trans>
187 </TextField.LabelText>
188 <TextField.Root>
189 <TextField.Input
190 label={_(msg`Confirmation code`)}
191 placeholder="XXXXX-XXXXX"
192 value={resetCode}
193 onChangeText={setResetCode}
194 onBlur={onBlur}
195 autoCapitalize="none"
196 autoCorrect={false}
197 autoComplete="one-time-code"
198 />
199 </TextField.Root>
200 </View>
201 <View>
202 <TextField.LabelText>
203 <Trans>New password</Trans>
204 </TextField.LabelText>
205 <TextField.Root>
206 <TextField.Input
207 label={_(msg`New password`)}
208 placeholder={_(msg`At least 8 characters`)}
209 value={newPassword}
210 onChangeText={setNewPassword}
211 secureTextEntry
212 autoCapitalize="none"
213 autoComplete="new-password"
214 passwordRules="minlength: 8;"
215 />
216 </TextField.Root>
217 </View>
218 </View>
219 )}
220
221 <View style={[a.gap_sm]}>
222 {stage === Stages.RequestCode ? (
223 <>
224 <Button
225 label={_(msg`Request code`)}
226 color="primary"
227 size="large"
228 disabled={isProcessing}
229 onPress={onRequestCode}>
230 <ButtonText>
231 <Trans>Request code</Trans>
232 </ButtonText>
233 {isProcessing && <ButtonIcon icon={Loader} />}
234 </Button>
235 <Button
236 label={_(msg`Already have a code?`)}
237 onPress={() => setStage(Stages.ChangePassword)}
238 size="large"
239 color="primary_subtle"
240 disabled={isProcessing}>
241 <ButtonText>
242 <Trans>Already have a code?</Trans>
243 </ButtonText>
244 </Button>
245 {IS_NATIVE && (
246 <Button
247 label={_(msg`Cancel`)}
248 color="secondary"
249 size="large"
250 disabled={isProcessing}
251 onPress={() => control.close()}>
252 <ButtonText>
253 <Trans>Cancel</Trans>
254 </ButtonText>
255 </Button>
256 )}
257 </>
258 ) : stage === Stages.ChangePassword ? (
259 <>
260 <Button
261 label={_(msg`Change password`)}
262 color="primary"
263 size="large"
264 disabled={isProcessing}
265 onPress={onChangePassword}>
266 <ButtonText>
267 <Trans>Change password</Trans>
268 </ButtonText>
269 {isProcessing && <ButtonIcon icon={Loader} />}
270 </Button>
271 <Button
272 label={_(msg`Back`)}
273 color="secondary"
274 size="large"
275 disabled={isProcessing}
276 onPress={() => {
277 setResetCode('')
278 setStage(Stages.RequestCode)
279 }}>
280 <ButtonText>
281 <Trans>Back</Trans>
282 </ButtonText>
283 </Button>
284 </>
285 ) : stage === Stages.Done ? (
286 <Button
287 label={_(msg`Close`)}
288 color="primary"
289 size="large"
290 onPress={() => control.close()}>
291 <ButtonText>
292 <Trans>Close</Trans>
293 </ButtonText>
294 </Button>
295 ) : null}
296 </View>
297 </View>
298 <Dialog.Close />
299 </Dialog.ScrollableInner>
300 )
301}