Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 3a9526d55eccacaf65fcbc885744d8ef4e50cf6a 361 lines 12 kB view raw
1import React, {useRef, useState} from 'react' 2import { 3 ActivityIndicator, 4 Keyboard, 5 LayoutAnimation, 6 type TextInput, 7 View, 8} from 'react-native' 9import { 10 ComAtprotoServerCreateSession, 11 type ComAtprotoServerDescribeServer, 12} from '@atproto/api' 13import {msg, Trans} from '@lingui/macro' 14import {useLingui} from '@lingui/react' 15 16import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' 17import {isNetworkError} from '#/lib/strings/errors' 18import {cleanError} from '#/lib/strings/errors' 19import {createFullHandle} from '#/lib/strings/handles' 20import {logger} from '#/logger' 21import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs' 22import {useSessionApi} from '#/state/session' 23import {useLoggedOutViewControls} from '#/state/shell/logged-out' 24import {atoms as a, ios, useTheme} from '#/alf' 25import {Button, ButtonIcon, ButtonText} from '#/components/Button' 26import {FormError} from '#/components/forms/FormError' 27import {HostingProvider} from '#/components/forms/HostingProvider' 28import * as TextField from '#/components/forms/TextField' 29import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' 30import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 31import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' 32import {Loader} from '#/components/Loader' 33import {Text} from '#/components/Typography' 34import {IS_IOS} from '#/env' 35import {FormContainer} from './FormContainer' 36 37type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema 38 39export const LoginForm = ({ 40 error, 41 serviceUrl, 42 serviceDescription, 43 initialHandle, 44 setError, 45 setServiceUrl, 46 onPressRetryConnect, 47 onPressBack, 48 onPressForgotPassword, 49 onAttemptSuccess, 50 onAttemptFailed, 51}: { 52 error: string 53 serviceUrl: string 54 serviceDescription: ServiceDescription | undefined 55 initialHandle: string 56 setError: (v: string) => void 57 setServiceUrl: (v: string) => void 58 onPressRetryConnect: () => void 59 onPressBack: () => void 60 onPressForgotPassword: () => void 61 onAttemptSuccess: () => void 62 onAttemptFailed: () => void 63}) => { 64 const t = useTheme() 65 const [isProcessing, setIsProcessing] = useState<boolean>(false) 66 const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = 67 useState<boolean>(false) 68 const identifierValueRef = useRef<string>(initialHandle || '') 69 const passwordValueRef = useRef<string>('') 70 const [authFactorToken, setAuthFactorToken] = useState('') 71 const identifierRef = useRef<TextInput>(null) 72 const passwordRef = useRef<TextInput>(null) 73 const hasFocusedOnce = useRef<boolean>(false) 74 const {_} = useLingui() 75 const {login} = useSessionApi() 76 const requestNotificationsPermission = useRequestNotificationsPermission() 77 const {setShowLoggedOut} = useLoggedOutViewControls() 78 const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack() 79 80 const onPressSelectService = React.useCallback(() => { 81 Keyboard.dismiss() 82 }, []) 83 84 const onPressNext = async () => { 85 if (isProcessing) return 86 Keyboard.dismiss() 87 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 88 setError('') 89 90 const identifier = identifierValueRef.current.toLowerCase().trim() 91 const password = passwordValueRef.current 92 93 if (!identifier) { 94 setError(_(msg`Please enter your username`)) 95 return 96 } 97 98 if (!password) { 99 setError(_(msg`Please enter your password`)) 100 return 101 } 102 103 setIsProcessing(true) 104 105 try { 106 // try to guess the handle if the user just gave their own username 107 let fullIdent = identifier 108 if ( 109 !identifier.includes('@') && // not an email 110 !identifier.includes('.') && // not a domain 111 serviceDescription && 112 serviceDescription.availableUserDomains.length > 0 113 ) { 114 let matched = false 115 for (const domain of serviceDescription.availableUserDomains) { 116 if (fullIdent.endsWith(domain)) { 117 matched = true 118 } 119 } 120 if (!matched) { 121 fullIdent = createFullHandle( 122 identifier, 123 serviceDescription.availableUserDomains[0], 124 ) 125 } 126 } 127 128 // TODO remove double login 129 await login( 130 { 131 service: serviceUrl, 132 identifier: fullIdent, 133 password, 134 authFactorToken: authFactorToken.trim(), 135 }, 136 'LoginForm', 137 ) 138 onAttemptSuccess() 139 setShowLoggedOut(false) 140 setHasCheckedForStarterPack(true) 141 requestNotificationsPermission('Login') 142 } catch (e: any) { 143 const errMsg = e.toString() 144 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 145 setIsProcessing(false) 146 if ( 147 e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError 148 ) { 149 setIsAuthFactorTokenNeeded(true) 150 } else { 151 onAttemptFailed() 152 if (errMsg.includes('Token is invalid')) { 153 logger.debug('Failed to login due to invalid 2fa token', { 154 error: errMsg, 155 }) 156 setError(_(msg`Invalid 2FA confirmation code.`)) 157 } else if ( 158 errMsg.includes('Authentication Required') || 159 errMsg.includes('Invalid identifier or password') 160 ) { 161 logger.debug('Failed to login due to invalid credentials', { 162 error: errMsg, 163 }) 164 setError(_(msg`Incorrect username or password`)) 165 } else if (isNetworkError(e)) { 166 logger.warn('Failed to login due to network error', {error: errMsg}) 167 setError( 168 _( 169 msg`Unable to contact your service. Please check your Internet connection.`, 170 ), 171 ) 172 } else { 173 logger.warn('Failed to login', {error: errMsg}) 174 setError(cleanError(errMsg)) 175 } 176 } 177 } 178 } 179 180 return ( 181 <FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}> 182 <View> 183 <TextField.LabelText> 184 <Trans>Hosting provider</Trans> 185 </TextField.LabelText> 186 <HostingProvider 187 serviceUrl={serviceUrl} 188 onSelectServiceUrl={setServiceUrl} 189 onOpenDialog={onPressSelectService} 190 /> 191 </View> 192 <View> 193 <TextField.LabelText> 194 <Trans>Account</Trans> 195 </TextField.LabelText> 196 <View style={[a.gap_sm]}> 197 <TextField.Root> 198 <TextField.Icon icon={At} /> 199 <TextField.Input 200 testID="loginUsernameInput" 201 inputRef={identifierRef} 202 label={_(msg`Username or email address`)} 203 autoCapitalize="none" 204 autoFocus={!IS_IOS} 205 autoCorrect={false} 206 autoComplete="username" 207 returnKeyType="next" 208 textContentType="username" 209 defaultValue={initialHandle || ''} 210 onChangeText={v => { 211 identifierValueRef.current = v 212 }} 213 onSubmitEditing={() => { 214 passwordRef.current?.focus() 215 }} 216 blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field 217 editable={!isProcessing} 218 accessibilityHint={_( 219 msg`Enter the username or email address you used when you created your account`, 220 )} 221 /> 222 </TextField.Root> 223 224 <TextField.Root> 225 <TextField.Icon icon={Lock} /> 226 <TextField.Input 227 testID="loginPasswordInput" 228 inputRef={passwordRef} 229 label={_(msg`Password`)} 230 autoCapitalize="none" 231 autoCorrect={false} 232 autoComplete="current-password" 233 returnKeyType="done" 234 enablesReturnKeyAutomatically={true} 235 secureTextEntry={true} 236 clearButtonMode="while-editing" 237 onChangeText={v => { 238 passwordValueRef.current = v 239 }} 240 onSubmitEditing={onPressNext} 241 blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing 242 editable={!isProcessing} 243 accessibilityHint={_(msg`Enter your password`)} 244 onLayout={ios(() => { 245 if (hasFocusedOnce.current) return 246 hasFocusedOnce.current = true 247 // kinda dumb, but if we use `autoFocus` to focus 248 // the username input, it happens before the password 249 // input gets rendered. this breaks the password autofill 250 // on iOS (it only does the username part). delaying 251 // it until both inputs are rendered fixes the autofill -sfn 252 identifierRef.current?.focus() 253 })} 254 /> 255 <Button 256 testID="forgotPasswordButton" 257 onPress={onPressForgotPassword} 258 label={_(msg`Forgot password?`)} 259 accessibilityHint={_(msg`Opens password reset form`)} 260 variant="solid" 261 color="secondary" 262 style={[ 263 a.rounded_sm, 264 // t.atoms.bg_contrast_100, 265 {marginLeft: 'auto', left: 6, padding: 6}, 266 a.z_10, 267 ]}> 268 <ButtonText> 269 <Trans>Forgot?</Trans> 270 </ButtonText> 271 </Button> 272 </TextField.Root> 273 </View> 274 </View> 275 {isAuthFactorTokenNeeded && ( 276 <View> 277 <TextField.LabelText> 278 <Trans>2FA Confirmation</Trans> 279 </TextField.LabelText> 280 <TextField.Root> 281 <TextField.Icon icon={Ticket} /> 282 <TextField.Input 283 testID="loginAuthFactorTokenInput" 284 label={_(msg`Confirmation code`)} 285 autoCapitalize="none" 286 autoFocus 287 autoCorrect={false} 288 autoComplete="one-time-code" 289 returnKeyType="done" 290 blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field 291 onChangeText={setAuthFactorToken} 292 value={authFactorToken} // controlled input due to uncontrolled input not receiving pasted values properly 293 onSubmitEditing={onPressNext} 294 editable={!isProcessing} 295 accessibilityHint={_( 296 msg`Input the code which has been emailed to you`, 297 )} 298 style={{ 299 textTransform: authFactorToken === '' ? 'none' : 'uppercase', 300 }} 301 /> 302 </TextField.Root> 303 <Text style={[a.text_sm, t.atoms.text_contrast_medium, a.mt_sm]}> 304 <Trans> 305 Check your email for a sign in code and enter it here. 306 </Trans> 307 </Text> 308 </View> 309 )} 310 <FormError error={error} /> 311 <View style={[a.flex_row, a.align_center, a.pt_md]}> 312 <Button 313 label={_(msg`Back`)} 314 variant="solid" 315 color="secondary" 316 size="large" 317 onPress={onPressBack}> 318 <ButtonText> 319 <Trans>Back</Trans> 320 </ButtonText> 321 </Button> 322 <View style={a.flex_1} /> 323 {!serviceDescription && error ? ( 324 <Button 325 testID="loginRetryButton" 326 label={_(msg`Retry`)} 327 accessibilityHint={_(msg`Retries signing in`)} 328 variant="solid" 329 color="secondary" 330 size="large" 331 onPress={onPressRetryConnect}> 332 <ButtonText> 333 <Trans>Retry</Trans> 334 </ButtonText> 335 </Button> 336 ) : !serviceDescription ? ( 337 <> 338 <ActivityIndicator /> 339 <Text style={[t.atoms.text_contrast_high, a.pl_md]}> 340 <Trans>Connecting...</Trans> 341 </Text> 342 </> 343 ) : ( 344 <Button 345 testID="loginNextButton" 346 label={_(msg`Next`)} 347 accessibilityHint={_(msg`Navigates to the next screen`)} 348 variant="solid" 349 color="primary" 350 size="large" 351 onPress={onPressNext}> 352 <ButtonText> 353 <Trans>Next</Trans> 354 </ButtonText> 355 {isProcessing && <ButtonIcon icon={Loader} />} 356 </Button> 357 )} 358 </View> 359 </FormContainer> 360 ) 361}