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