Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

Improve textinput performance in login and account creation (#4673)

* Change login form to use uncontrolled inputs

* Debounce state updates in account creation to reduce flicker

* Refactor state-control of account creation forms to fix perf without relying on debounces

* Remove canNext and enforce is13

* Re-add live validation to signup form (#4720)

* Update validation in real time

* Disable on invalid

* Clear server error on typing

* Remove unnecessary clearing of error

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>

authored by

Paul Frazee
Dan Abramov
and committed by
GitHub
63bb8fda 4bb4452f

+357 -269
+40 -20
src/screens/Login/LoginForm.tsx
··· 60 60 const {track} = useAnalytics() 61 61 const t = useTheme() 62 62 const [isProcessing, setIsProcessing] = useState<boolean>(false) 63 + const [isReady, setIsReady] = useState<boolean>(false) 63 64 const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = 64 65 useState<boolean>(false) 65 - const [identifier, setIdentifier] = useState<string>(initialHandle) 66 - const [password, setPassword] = useState<string>('') 67 - const [authFactorToken, setAuthFactorToken] = useState<string>('') 68 - const passwordInputRef = useRef<TextInput>(null) 66 + const identifierValueRef = useRef<string>(initialHandle || '') 67 + const passwordValueRef = useRef<string>('') 68 + const authFactorTokenValueRef = useRef<string>('') 69 + const passwordRef = useRef<TextInput>(null) 69 70 const {_} = useLingui() 70 71 const {login} = useSessionApi() 71 72 const requestNotificationsPermission = useRequestNotificationsPermission() ··· 83 84 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 84 85 setError('') 85 86 setIsProcessing(true) 87 + 88 + const identifier = identifierValueRef.current.toLowerCase().trim() 89 + const password = passwordValueRef.current 90 + const authFactorToken = authFactorTokenValueRef.current 86 91 87 92 try { 88 93 // try to guess the handle if the user just gave their own username ··· 152 157 } 153 158 } 154 159 155 - const isReady = !!serviceDescription && !!identifier && !!password 160 + const checkIsReady = () => { 161 + if ( 162 + !!serviceDescription && 163 + !!identifierValueRef.current && 164 + !!passwordValueRef.current 165 + ) { 166 + if (!isReady) { 167 + setIsReady(true) 168 + } 169 + } else { 170 + if (isReady) { 171 + setIsReady(false) 172 + } 173 + } 174 + } 175 + 156 176 return ( 157 177 <FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}> 158 178 <View> ··· 181 201 autoComplete="username" 182 202 returnKeyType="next" 183 203 textContentType="username" 204 + defaultValue={initialHandle || ''} 205 + onChangeText={v => { 206 + identifierValueRef.current = v 207 + checkIsReady() 208 + }} 184 209 onSubmitEditing={() => { 185 - passwordInputRef.current?.focus() 210 + passwordRef.current?.focus() 186 211 }} 187 212 blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field 188 - value={identifier} 189 - onChangeText={str => 190 - setIdentifier((str || '').toLowerCase().trim()) 191 - } 192 213 editable={!isProcessing} 193 214 accessibilityHint={_( 194 215 msg`Input the username or email address you used at signup`, ··· 200 221 <TextField.Icon icon={Lock} /> 201 222 <TextField.Input 202 223 testID="loginPasswordInput" 203 - inputRef={passwordInputRef} 224 + inputRef={passwordRef} 204 225 label={_(msg`Password`)} 205 226 autoCapitalize="none" 206 227 autoCorrect={false} ··· 210 231 secureTextEntry={true} 211 232 textContentType="password" 212 233 clearButtonMode="while-editing" 213 - value={password} 214 - onChangeText={setPassword} 234 + onChangeText={v => { 235 + passwordValueRef.current = v 236 + checkIsReady() 237 + }} 215 238 onSubmitEditing={onPressNext} 216 239 blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing 217 240 editable={!isProcessing} 218 - accessibilityHint={ 219 - identifier === '' 220 - ? _(msg`Input your password`) 221 - : _(msg`Input the password tied to ${identifier}`) 222 - } 241 + accessibilityHint={_(msg`Input your password`)} 223 242 /> 224 243 <Button 225 244 testID="forgotPasswordButton" ··· 258 277 returnKeyType="done" 259 278 textContentType="username" 260 279 blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field 261 - value={authFactorToken} 262 - onChangeText={setAuthFactorToken} 280 + onChangeText={v => { 281 + authFactorTokenValueRef.current = v 282 + }} 263 283 onSubmitEditing={onPressNext} 264 284 editable={!isProcessing} 265 285 accessibilityHint={_(
+73
src/screens/Signup/BackNextButtons.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {msg, Trans} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 + 6 + import {atoms as a} from '#/alf' 7 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 8 + import {Loader} from '#/components/Loader' 9 + 10 + export interface BackNextButtonsProps { 11 + hideNext?: boolean 12 + showRetry?: boolean 13 + isLoading: boolean 14 + isNextDisabled?: boolean 15 + onBackPress: () => void 16 + onNextPress?: () => void 17 + onRetryPress?: () => void 18 + } 19 + 20 + export function BackNextButtons({ 21 + hideNext, 22 + showRetry, 23 + isLoading, 24 + isNextDisabled, 25 + onBackPress, 26 + onNextPress, 27 + onRetryPress, 28 + }: BackNextButtonsProps) { 29 + const {_} = useLingui() 30 + 31 + return ( 32 + <View style={[a.flex_row, a.justify_between, a.pb_lg, a.pt_3xl]}> 33 + <Button 34 + label={_(msg`Go back to previous step`)} 35 + variant="solid" 36 + color="secondary" 37 + size="medium" 38 + onPress={onBackPress}> 39 + <ButtonText> 40 + <Trans>Back</Trans> 41 + </ButtonText> 42 + </Button> 43 + {!hideNext && 44 + (showRetry ? ( 45 + <Button 46 + label={_(msg`Press to retry`)} 47 + variant="solid" 48 + color="primary" 49 + size="medium" 50 + onPress={onRetryPress}> 51 + <ButtonText> 52 + <Trans>Retry</Trans> 53 + </ButtonText> 54 + {isLoading && <ButtonIcon icon={Loader} />} 55 + </Button> 56 + ) : ( 57 + <Button 58 + testID="nextBtn" 59 + label={_(msg`Continue to next step`)} 60 + variant="solid" 61 + color="primary" 62 + size="medium" 63 + disabled={isLoading || isNextDisabled} 64 + onPress={onNextPress}> 65 + <ButtonText> 66 + <Trans>Next</Trans> 67 + </ButtonText> 68 + {isLoading && <ButtonIcon icon={Loader} />} 69 + </Button> 70 + ))} 71 + </View> 72 + ) 73 + }
+16
src/screens/Signup/StepCaptcha/index.tsx
··· 12 12 import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView' 13 13 import {atoms as a, useTheme} from '#/alf' 14 14 import {FormError} from '#/components/forms/FormError' 15 + import {BackNextButtons} from '../BackNextButtons' 15 16 16 17 const CAPTCHA_PATH = '/gate/signup' 17 18 ··· 61 62 [_, dispatch, state.handle], 62 63 ) 63 64 65 + const onBackPress = React.useCallback(() => { 66 + logger.error('Signup Flow Error', { 67 + errorMessage: 68 + 'User went back from captcha step. Possibly encountered an error.', 69 + registrationHandle: state.handle, 70 + }) 71 + 72 + dispatch({type: 'prev'}) 73 + }, [dispatch, state.handle]) 74 + 64 75 return ( 65 76 <ScreenTransition> 66 77 <View style={[a.gap_lg]}> ··· 86 97 </View> 87 98 <FormError error={state.error} /> 88 99 </View> 100 + <BackNextButtons 101 + hideNext 102 + isLoading={state.isLoading} 103 + onBackPress={onBackPress} 104 + /> 89 105 </ScreenTransition> 90 106 ) 91 107 }
+138 -80
src/screens/Signup/StepHandle.tsx
··· 1 - import React from 'react' 1 + import React, {useRef} from 'react' 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 - import {useFocusEffect} from '@react-navigation/native' 6 5 7 - import { 8 - createFullHandle, 9 - IsValidHandle, 10 - validateHandle, 11 - } from '#/lib/strings/handles' 6 + import {logEvent} from '#/lib/statsig/statsig' 7 + import {createFullHandle, validateHandle} from '#/lib/strings/handles' 8 + import {useAgent} from '#/state/session' 12 9 import {ScreenTransition} from '#/screens/Login/ScreenTransition' 13 - import {useSignupContext} from '#/screens/Signup/state' 10 + import {useSignupContext, useSubmitSignup} from '#/screens/Signup/state' 14 11 import {atoms as a, useTheme} from '#/alf' 15 12 import * as TextField from '#/components/forms/TextField' 16 13 import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' 17 14 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 18 15 import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' 19 16 import {Text} from '#/components/Typography' 17 + import {BackNextButtons} from './BackNextButtons' 20 18 21 19 export function StepHandle() { 22 20 const {_} = useLingui() 23 21 const t = useTheme() 24 22 const {state, dispatch} = useSignupContext() 23 + const submit = useSubmitSignup({state, dispatch}) 24 + const agent = useAgent() 25 + const handleValueRef = useRef<string>(state.handle) 26 + const [draftValue, setDraftValue] = React.useState(state.handle) 25 27 26 - const [validCheck, setValidCheck] = React.useState<IsValidHandle>({ 27 - handleChars: false, 28 - hyphenStartOrEnd: false, 29 - frontLength: false, 30 - totalLength: true, 31 - overall: false, 32 - }) 28 + const onNextPress = React.useCallback(async () => { 29 + const handle = handleValueRef.current.trim() 30 + dispatch({ 31 + type: 'setHandle', 32 + value: handle, 33 + }) 34 + 35 + const newValidCheck = validateHandle(handle, state.userDomain) 36 + if (!newValidCheck.overall) { 37 + return 38 + } 33 39 34 - useFocusEffect( 35 - React.useCallback(() => { 36 - setValidCheck(validateHandle(state.handle, state.userDomain)) 37 - }, [state.handle, state.userDomain]), 38 - ) 40 + try { 41 + dispatch({type: 'setIsLoading', value: true}) 42 + 43 + const res = await agent.resolveHandle({ 44 + handle: createFullHandle(handle, state.userDomain), 45 + }) 39 46 40 - const onHandleChange = React.useCallback( 41 - (value: string) => { 42 - if (state.error) { 43 - dispatch({type: 'setError', value: ''}) 47 + if (res.data.did) { 48 + dispatch({ 49 + type: 'setError', 50 + value: _(msg`That handle is already taken.`), 51 + }) 52 + return 44 53 } 54 + } catch (e) { 55 + // Don't have to handle 56 + } finally { 57 + dispatch({type: 'setIsLoading', value: false}) 58 + } 45 59 46 - dispatch({ 47 - type: 'setHandle', 48 - value, 49 - }) 50 - }, 51 - [dispatch, state.error], 52 - ) 60 + logEvent('signup:nextPressed', { 61 + activeStep: state.activeStep, 62 + phoneVerificationRequired: 63 + state.serviceDescription?.phoneVerificationRequired, 64 + }) 65 + // phoneVerificationRequired is actually whether a captcha is required 66 + if (!state.serviceDescription?.phoneVerificationRequired) { 67 + submit() 68 + return 69 + } 70 + dispatch({type: 'next'}) 71 + }, [ 72 + _, 73 + dispatch, 74 + state.activeStep, 75 + state.serviceDescription?.phoneVerificationRequired, 76 + state.userDomain, 77 + submit, 78 + agent, 79 + ]) 53 80 81 + const onBackPress = React.useCallback(() => { 82 + const handle = handleValueRef.current.trim() 83 + dispatch({ 84 + type: 'setHandle', 85 + value: handle, 86 + }) 87 + dispatch({type: 'prev'}) 88 + logEvent('signup:backPressed', { 89 + activeStep: state.activeStep, 90 + }) 91 + }, [dispatch, state.activeStep]) 92 + 93 + const validCheck = validateHandle(draftValue, state.userDomain) 54 94 return ( 55 95 <ScreenTransition> 56 96 <View style={[a.gap_lg]}> ··· 59 99 <TextField.Icon icon={At} /> 60 100 <TextField.Input 61 101 testID="handleInput" 62 - onChangeText={onHandleChange} 102 + onChangeText={val => { 103 + if (state.error) { 104 + dispatch({type: 'setError', value: ''}) 105 + } 106 + 107 + // These need to always be in sync. 108 + handleValueRef.current = val 109 + setDraftValue(val) 110 + }} 63 111 label={_(msg`Input your user handle`)} 64 - defaultValue={state.handle} 112 + defaultValue={draftValue} 65 113 autoCapitalize="none" 66 114 autoCorrect={false} 67 115 autoFocus ··· 69 117 /> 70 118 </TextField.Root> 71 119 </View> 72 - <Text style={[a.text_md]}> 73 - <Trans>Your full handle will be</Trans>{' '} 74 - <Text style={[a.text_md, a.font_bold]}> 75 - @{createFullHandle(state.handle, state.userDomain)} 120 + {draftValue !== '' && ( 121 + <Text style={[a.text_md]}> 122 + <Trans>Your full handle will be</Trans>{' '} 123 + <Text style={[a.text_md, a.font_bold]}> 124 + @{createFullHandle(draftValue, state.userDomain)} 125 + </Text> 76 126 </Text> 77 - </Text> 127 + )} 78 128 79 - <View 80 - style={[ 81 - a.w_full, 82 - a.rounded_sm, 83 - a.border, 84 - a.p_md, 85 - a.gap_sm, 86 - t.atoms.border_contrast_low, 87 - ]}> 88 - {state.error ? ( 129 + {draftValue !== '' && ( 130 + <View 131 + style={[ 132 + a.w_full, 133 + a.rounded_sm, 134 + a.border, 135 + a.p_md, 136 + a.gap_sm, 137 + t.atoms.border_contrast_low, 138 + ]}> 139 + {state.error ? ( 140 + <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 141 + <IsValidIcon valid={false} /> 142 + <Text style={[a.text_md, a.flex_1]}>{state.error}</Text> 143 + </View> 144 + ) : undefined} 145 + {validCheck.hyphenStartOrEnd ? ( 146 + <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 147 + <IsValidIcon valid={validCheck.handleChars} /> 148 + <Text style={[a.text_md, a.flex_1]}> 149 + <Trans>Only contains letters, numbers, and hyphens</Trans> 150 + </Text> 151 + </View> 152 + ) : ( 153 + <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 154 + <IsValidIcon valid={validCheck.hyphenStartOrEnd} /> 155 + <Text style={[a.text_md, a.flex_1]}> 156 + <Trans>Doesn't begin or end with a hyphen</Trans> 157 + </Text> 158 + </View> 159 + )} 89 160 <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 90 - <IsValidIcon valid={false} /> 91 - <Text style={[a.text_md, a.flex_1]}>{state.error}</Text> 92 - </View> 93 - ) : undefined} 94 - {validCheck.hyphenStartOrEnd ? ( 95 - <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 96 - <IsValidIcon valid={validCheck.handleChars} /> 97 - <Text style={[a.text_md, a.flex_1]}> 98 - <Trans>Only contains letters, numbers, and hyphens</Trans> 99 - </Text> 100 - </View> 101 - ) : ( 102 - <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 103 - <IsValidIcon valid={validCheck.hyphenStartOrEnd} /> 104 - <Text style={[a.text_md, a.flex_1]}> 105 - <Trans>Doesn't begin or end with a hyphen</Trans> 106 - </Text> 161 + <IsValidIcon 162 + valid={validCheck.frontLength && validCheck.totalLength} 163 + /> 164 + {!validCheck.totalLength ? ( 165 + <Text style={[a.text_md, a.flex_1]}> 166 + <Trans>No longer than 253 characters</Trans> 167 + </Text> 168 + ) : ( 169 + <Text style={[a.text_md, a.flex_1]}> 170 + <Trans>At least 3 characters</Trans> 171 + </Text> 172 + )} 107 173 </View> 108 - )} 109 - <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 110 - <IsValidIcon 111 - valid={validCheck.frontLength && validCheck.totalLength} 112 - /> 113 - {!validCheck.totalLength ? ( 114 - <Text style={[a.text_md, a.flex_1]}> 115 - <Trans>No longer than 253 characters</Trans> 116 - </Text> 117 - ) : ( 118 - <Text style={[a.text_md, a.flex_1]}> 119 - <Trans>At least 3 characters</Trans> 120 - </Text> 121 - )} 122 174 </View> 123 - </View> 175 + )} 124 176 </View> 177 + <BackNextButtons 178 + isLoading={state.isLoading} 179 + isNextDisabled={!validCheck.overall} 180 + onBackPress={onBackPress} 181 + onNextPress={onNextPress} 182 + /> 125 183 </ScreenTransition> 126 184 ) 127 185 }
+74 -13
src/screens/Signup/StepInfo/index.tsx
··· 1 - import React from 'react' 1 + import React, {useRef} from 'react' 2 2 import {View} from 'react-native' 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 + import * as EmailValidator from 'email-validator' 5 6 7 + import {logEvent} from '#/lib/statsig/statsig' 6 8 import {logger} from '#/logger' 7 9 import {ScreenTransition} from '#/screens/Login/ScreenTransition' 8 10 import {is13, is18, useSignupContext} from '#/screens/Signup/state' ··· 16 18 import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 17 19 import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' 18 20 import {Loader} from '#/components/Loader' 21 + import {BackNextButtons} from '../BackNextButtons' 19 22 20 23 function sanitizeDate(date: Date): Date { 21 24 if (!date || date.toString() === 'Invalid Date') { ··· 28 31 } 29 32 30 33 export function StepInfo({ 34 + onPressBack, 35 + isServerError, 36 + refetchServer, 31 37 isLoadingStarterPack, 32 38 }: { 39 + onPressBack: () => void 40 + isServerError: boolean 41 + refetchServer: () => void 33 42 isLoadingStarterPack: boolean 34 43 }) { 35 44 const {_} = useLingui() 36 45 const {state, dispatch} = useSignupContext() 37 46 47 + const inviteCodeValueRef = useRef<string>(state.inviteCode) 48 + const emailValueRef = useRef<string>(state.email) 49 + const passwordValueRef = useRef<string>(state.password) 50 + 51 + const onNextPress = React.useCallback(async () => { 52 + const inviteCode = inviteCodeValueRef.current 53 + const email = emailValueRef.current 54 + const password = passwordValueRef.current 55 + 56 + if (!is13(state.dateOfBirth)) { 57 + return 58 + } 59 + 60 + if (state.serviceDescription?.inviteCodeRequired && !inviteCode) { 61 + return dispatch({ 62 + type: 'setError', 63 + value: _(msg`Please enter your invite code.`), 64 + }) 65 + } 66 + if (!email) { 67 + return dispatch({ 68 + type: 'setError', 69 + value: _(msg`Please enter your email.`), 70 + }) 71 + } 72 + if (!EmailValidator.validate(email)) { 73 + return dispatch({ 74 + type: 'setError', 75 + value: _(msg`Your email appears to be invalid.`), 76 + }) 77 + } 78 + if (!password) { 79 + return dispatch({ 80 + type: 'setError', 81 + value: _(msg`Please choose your password.`), 82 + }) 83 + } 84 + 85 + dispatch({type: 'setInviteCode', value: inviteCode}) 86 + dispatch({type: 'setEmail', value: email}) 87 + dispatch({type: 'setPassword', value: password}) 88 + dispatch({type: 'next'}) 89 + logEvent('signup:nextPressed', { 90 + activeStep: state.activeStep, 91 + }) 92 + }, [ 93 + _, 94 + dispatch, 95 + state.activeStep, 96 + state.dateOfBirth, 97 + state.serviceDescription?.inviteCodeRequired, 98 + ]) 99 + 38 100 return ( 39 101 <ScreenTransition> 40 102 <View style={[a.gap_md]}> ··· 65 127 <TextField.Icon icon={Ticket} /> 66 128 <TextField.Input 67 129 onChangeText={value => { 68 - dispatch({ 69 - type: 'setInviteCode', 70 - value: value.trim(), 71 - }) 130 + inviteCodeValueRef.current = value.trim() 72 131 }} 73 132 label={_(msg`Required for this provider`)} 74 133 defaultValue={state.inviteCode} ··· 88 147 <TextField.Input 89 148 testID="emailInput" 90 149 onChangeText={value => { 91 - dispatch({ 92 - type: 'setEmail', 93 - value: value.trim(), 94 - }) 150 + emailValueRef.current = value.trim() 95 151 }} 96 152 label={_(msg`Enter your email address`)} 97 153 defaultValue={state.email} ··· 110 166 <TextField.Input 111 167 testID="passwordInput" 112 168 onChangeText={value => { 113 - dispatch({ 114 - type: 'setPassword', 115 - value, 116 - }) 169 + passwordValueRef.current = value 117 170 }} 118 171 label={_(msg`Choose your password`)} 119 172 defaultValue={state.password} ··· 147 200 </> 148 201 ) : undefined} 149 202 </View> 203 + <BackNextButtons 204 + hideNext={!is13(state.dateOfBirth)} 205 + showRetry={isServerError} 206 + isLoading={state.isLoading} 207 + onBackPress={onPressBack} 208 + onNextPress={onNextPress} 209 + onRetryPress={refetchServer} 210 + /> 150 211 </ScreenTransition> 151 212 ) 152 213 }
+15 -131
src/screens/Signup/index.tsx
··· 7 7 8 8 import {useAnalytics} from '#/lib/analytics/analytics' 9 9 import {FEEDBACK_FORM_URL} from '#/lib/constants' 10 - import {logEvent} from '#/lib/statsig/statsig' 11 - import {createFullHandle} from '#/lib/strings/handles' 12 - import {logger} from '#/logger' 13 10 import {useServiceQuery} from '#/state/queries/service' 14 - import {useAgent} from '#/state/session' 15 11 import {useStarterPackQuery} from 'state/queries/starter-packs' 16 12 import {useActiveStarterPack} from 'state/shell/starter-pack' 17 13 import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' ··· 20 16 reducer, 21 17 SignupContext, 22 18 SignupStep, 23 - useSubmitSignup, 24 19 } from '#/screens/Signup/state' 25 20 import {StepCaptcha} from '#/screens/Signup/StepCaptcha' 26 21 import {StepHandle} from '#/screens/Signup/StepHandle' 27 22 import {StepInfo} from '#/screens/Signup/StepInfo' 28 23 import {atoms as a, useBreakpoints, useTheme} from '#/alf' 29 24 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 30 - import {Button, ButtonText} from '#/components/Button' 31 25 import {Divider} from '#/components/Divider' 32 26 import {LinearGradientBackground} from '#/components/LinearGradientBackground' 33 27 import {InlineLinkText} from '#/components/Link' ··· 38 32 const t = useTheme() 39 33 const {screen} = useAnalytics() 40 34 const [state, dispatch] = React.useReducer(reducer, initialState) 41 - const submit = useSubmitSignup({state, dispatch}) 42 35 const {gtMobile} = useBreakpoints() 43 - const agent = useAgent() 44 36 45 37 const activeStarterPack = useActiveStarterPack() 46 38 const { ··· 89 81 } 90 82 }, [_, serviceInfo, isError]) 91 83 92 - const onNextPress = React.useCallback(async () => { 93 - if (state.activeStep === SignupStep.HANDLE) { 94 - try { 95 - dispatch({type: 'setIsLoading', value: true}) 96 - 97 - const res = await agent.resolveHandle({ 98 - handle: createFullHandle(state.handle, state.userDomain), 99 - }) 100 - 101 - if (res.data.did) { 102 - dispatch({ 103 - type: 'setError', 104 - value: _(msg`That handle is already taken.`), 105 - }) 106 - return 107 - } 108 - } catch (e) { 109 - // Don't have to handle 110 - } finally { 111 - dispatch({type: 'setIsLoading', value: false}) 112 - } 113 - } 114 - 115 - logEvent('signup:nextPressed', { 116 - activeStep: state.activeStep, 117 - phoneVerificationRequired: 118 - state.serviceDescription?.phoneVerificationRequired, 119 - }) 120 - 121 - // phoneVerificationRequired is actually whether a captcha is required 122 - if ( 123 - state.activeStep === SignupStep.HANDLE && 124 - !state.serviceDescription?.phoneVerificationRequired 125 - ) { 126 - submit() 127 - return 128 - } 129 - dispatch({type: 'next'}) 130 - }, [ 131 - _, 132 - state.activeStep, 133 - state.handle, 134 - state.serviceDescription?.phoneVerificationRequired, 135 - state.userDomain, 136 - submit, 137 - agent, 138 - ]) 139 - 140 - const onBackPress = React.useCallback(() => { 141 - if (state.activeStep !== SignupStep.INFO) { 142 - if (state.activeStep === SignupStep.CAPTCHA) { 143 - logger.error('Signup Flow Error', { 144 - errorMessage: 145 - 'User went back from captcha step. Possibly encountered an error.', 146 - registrationHandle: state.handle, 147 - }) 148 - } 149 - dispatch({type: 'prev'}) 150 - } else { 151 - onPressBack() 152 - } 153 - logEvent('signup:backPressed', { 154 - activeStep: state.activeStep, 155 - }) 156 - }, [onPressBack, state.activeStep, state.handle]) 157 - 158 84 return ( 159 85 <SignupContext.Provider value={{state, dispatch}}> 160 86 <LoggedOutLayout ··· 215 141 </Text> 216 142 </View> 217 143 218 - <View style={[a.pb_3xl]}> 219 - <LayoutAnimationConfig skipEntering skipExiting> 220 - {state.activeStep === SignupStep.INFO ? ( 221 - <StepInfo 222 - isLoadingStarterPack={ 223 - isFetchingStarterPack && !isErrorStarterPack 224 - } 225 - /> 226 - ) : state.activeStep === SignupStep.HANDLE ? ( 227 - <StepHandle /> 228 - ) : ( 229 - <StepCaptcha /> 230 - )} 231 - </LayoutAnimationConfig> 232 - </View> 233 - 234 - <View style={[a.flex_row, a.justify_between, a.pb_lg]}> 235 - <Button 236 - label={_(msg`Go back to previous step`)} 237 - variant="solid" 238 - color="secondary" 239 - size="medium" 240 - onPress={onBackPress}> 241 - <ButtonText> 242 - <Trans>Back</Trans> 243 - </ButtonText> 244 - </Button> 245 - {state.activeStep !== SignupStep.CAPTCHA && ( 246 - <> 247 - {isError ? ( 248 - <Button 249 - label={_(msg`Press to retry`)} 250 - variant="solid" 251 - color="primary" 252 - size="medium" 253 - disabled={state.isLoading} 254 - onPress={() => refetch()}> 255 - <ButtonText> 256 - <Trans>Retry</Trans> 257 - </ButtonText> 258 - </Button> 259 - ) : ( 260 - <Button 261 - testID="nextBtn" 262 - label={_(msg`Continue to next step`)} 263 - variant="solid" 264 - color="primary" 265 - size="medium" 266 - disabled={!state.canNext || state.isLoading} 267 - onPress={onNextPress}> 268 - <ButtonText> 269 - <Trans>Next</Trans> 270 - </ButtonText> 271 - </Button> 272 - )} 273 - </> 144 + <LayoutAnimationConfig skipEntering skipExiting> 145 + {state.activeStep === SignupStep.INFO ? ( 146 + <StepInfo 147 + onPressBack={onPressBack} 148 + isLoadingStarterPack={ 149 + isFetchingStarterPack && !isErrorStarterPack 150 + } 151 + isServerError={isError} 152 + refetchServer={refetch} 153 + /> 154 + ) : state.activeStep === SignupStep.HANDLE ? ( 155 + <StepHandle /> 156 + ) : ( 157 + <StepCaptcha /> 274 158 )} 275 - </View> 159 + </LayoutAnimationConfig> 276 160 277 161 <Divider /> 278 162
+1 -25
src/screens/Signup/state.ts
··· 10 10 11 11 import {DEFAULT_SERVICE} from '#/lib/constants' 12 12 import {cleanError} from '#/lib/strings/errors' 13 - import {createFullHandle, validateHandle} from '#/lib/strings/handles' 13 + import {createFullHandle} from '#/lib/strings/handles' 14 14 import {getAge} from '#/lib/strings/time' 15 15 import {logger} from '#/logger' 16 16 import {useSessionApi} from '#/state/session' ··· 28 28 29 29 export type SignupState = { 30 30 hasPrev: boolean 31 - canNext: boolean 32 31 activeStep: SignupStep 33 32 34 33 serviceUrl: string ··· 58 57 | {type: 'setHandle'; value: string} 59 58 | {type: 'setVerificationCode'; value: string} 60 59 | {type: 'setError'; value: string} 61 - | {type: 'setCanNext'; value: boolean} 62 60 | {type: 'setIsLoading'; value: boolean} 63 61 64 62 export const initialState: SignupState = { 65 63 hasPrev: false, 66 - canNext: false, 67 64 activeStep: SignupStep.INFO, 68 65 69 66 serviceUrl: DEFAULT_SERVICE, ··· 144 141 next.handle = a.value 145 142 break 146 143 } 147 - case 'setCanNext': { 148 - next.canNext = a.value 149 - break 150 - } 151 144 case 'setIsLoading': { 152 145 next.isLoading = a.value 153 146 break ··· 159 152 } 160 153 161 154 next.hasPrev = next.activeStep !== SignupStep.INFO 162 - 163 - switch (next.activeStep) { 164 - case SignupStep.INFO: { 165 - const isValidEmail = EmailValidator.validate(next.email) 166 - next.canNext = 167 - !!(next.email && next.password && next.dateOfBirth) && 168 - (!next.serviceDescription?.inviteCodeRequired || !!next.inviteCode) && 169 - is13(next.dateOfBirth) && 170 - isValidEmail 171 - break 172 - } 173 - case SignupStep.HANDLE: { 174 - next.canNext = 175 - !!next.handle && validateHandle(next.handle, next.userDomain).overall 176 - break 177 - } 178 - } 179 155 180 156 logger.debug('signup', next) 181 157