Bluesky app fork with some witchin' additions 馃挮
at main 302 lines 9.2 kB view raw
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}