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>
zio/stable
Paul Frazee 2024-07-02 14:43:34 -07:00 committed by GitHub
parent 4bb4452f08
commit 63bb8fda2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 357 additions and 269 deletions

View File

@ -60,12 +60,13 @@ export const LoginForm = ({
const {track} = useAnalytics() const {track} = useAnalytics()
const t = useTheme() const t = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false) const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [isReady, setIsReady] = useState<boolean>(false)
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
useState<boolean>(false) useState<boolean>(false)
const [identifier, setIdentifier] = useState<string>(initialHandle) const identifierValueRef = useRef<string>(initialHandle || '')
const [password, setPassword] = useState<string>('') const passwordValueRef = useRef<string>('')
const [authFactorToken, setAuthFactorToken] = useState<string>('') const authFactorTokenValueRef = useRef<string>('')
const passwordInputRef = useRef<TextInput>(null) const passwordRef = useRef<TextInput>(null)
const {_} = useLingui() const {_} = useLingui()
const {login} = useSessionApi() const {login} = useSessionApi()
const requestNotificationsPermission = useRequestNotificationsPermission() const requestNotificationsPermission = useRequestNotificationsPermission()
@ -84,6 +85,10 @@ export const LoginForm = ({
setError('') setError('')
setIsProcessing(true) setIsProcessing(true)
const identifier = identifierValueRef.current.toLowerCase().trim()
const password = passwordValueRef.current
const authFactorToken = authFactorTokenValueRef.current
try { try {
// try to guess the handle if the user just gave their own username // try to guess the handle if the user just gave their own username
let fullIdent = identifier let fullIdent = identifier
@ -152,7 +157,22 @@ export const LoginForm = ({
} }
} }
const isReady = !!serviceDescription && !!identifier && !!password const checkIsReady = () => {
if (
!!serviceDescription &&
!!identifierValueRef.current &&
!!passwordValueRef.current
) {
if (!isReady) {
setIsReady(true)
}
} else {
if (isReady) {
setIsReady(false)
}
}
}
return ( return (
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}> <FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
<View> <View>
@ -181,14 +201,15 @@ export const LoginForm = ({
autoComplete="username" autoComplete="username"
returnKeyType="next" returnKeyType="next"
textContentType="username" textContentType="username"
defaultValue={initialHandle || ''}
onChangeText={v => {
identifierValueRef.current = v
checkIsReady()
}}
onSubmitEditing={() => { onSubmitEditing={() => {
passwordInputRef.current?.focus() passwordRef.current?.focus()
}} }}
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
value={identifier}
onChangeText={str =>
setIdentifier((str || '').toLowerCase().trim())
}
editable={!isProcessing} editable={!isProcessing}
accessibilityHint={_( accessibilityHint={_(
msg`Input the username or email address you used at signup`, msg`Input the username or email address you used at signup`,
@ -200,7 +221,7 @@ export const LoginForm = ({
<TextField.Icon icon={Lock} /> <TextField.Icon icon={Lock} />
<TextField.Input <TextField.Input
testID="loginPasswordInput" testID="loginPasswordInput"
inputRef={passwordInputRef} inputRef={passwordRef}
label={_(msg`Password`)} label={_(msg`Password`)}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
@ -210,16 +231,14 @@ export const LoginForm = ({
secureTextEntry={true} secureTextEntry={true}
textContentType="password" textContentType="password"
clearButtonMode="while-editing" clearButtonMode="while-editing"
value={password} onChangeText={v => {
onChangeText={setPassword} passwordValueRef.current = v
checkIsReady()
}}
onSubmitEditing={onPressNext} onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
editable={!isProcessing} editable={!isProcessing}
accessibilityHint={ accessibilityHint={_(msg`Input your password`)}
identifier === ''
? _(msg`Input your password`)
: _(msg`Input the password tied to ${identifier}`)
}
/> />
<Button <Button
testID="forgotPasswordButton" testID="forgotPasswordButton"
@ -258,8 +277,9 @@ export const LoginForm = ({
returnKeyType="done" returnKeyType="done"
textContentType="username" textContentType="username"
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
value={authFactorToken} onChangeText={v => {
onChangeText={setAuthFactorToken} authFactorTokenValueRef.current = v
}}
onSubmitEditing={onPressNext} onSubmitEditing={onPressNext}
editable={!isProcessing} editable={!isProcessing}
accessibilityHint={_( accessibilityHint={_(

View File

@ -0,0 +1,73 @@
import React from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {Loader} from '#/components/Loader'
export interface BackNextButtonsProps {
hideNext?: boolean
showRetry?: boolean
isLoading: boolean
isNextDisabled?: boolean
onBackPress: () => void
onNextPress?: () => void
onRetryPress?: () => void
}
export function BackNextButtons({
hideNext,
showRetry,
isLoading,
isNextDisabled,
onBackPress,
onNextPress,
onRetryPress,
}: BackNextButtonsProps) {
const {_} = useLingui()
return (
<View style={[a.flex_row, a.justify_between, a.pb_lg, a.pt_3xl]}>
<Button
label={_(msg`Go back to previous step`)}
variant="solid"
color="secondary"
size="medium"
onPress={onBackPress}>
<ButtonText>
<Trans>Back</Trans>
</ButtonText>
</Button>
{!hideNext &&
(showRetry ? (
<Button
label={_(msg`Press to retry`)}
variant="solid"
color="primary"
size="medium"
onPress={onRetryPress}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
{isLoading && <ButtonIcon icon={Loader} />}
</Button>
) : (
<Button
testID="nextBtn"
label={_(msg`Continue to next step`)}
variant="solid"
color="primary"
size="medium"
disabled={isLoading || isNextDisabled}
onPress={onNextPress}>
<ButtonText>
<Trans>Next</Trans>
</ButtonText>
{isLoading && <ButtonIcon icon={Loader} />}
</Button>
))}
</View>
)
}

View File

@ -12,6 +12,7 @@ import {useSignupContext, useSubmitSignup} from '#/screens/Signup/state'
import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView' import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {FormError} from '#/components/forms/FormError' import {FormError} from '#/components/forms/FormError'
import {BackNextButtons} from '../BackNextButtons'
const CAPTCHA_PATH = '/gate/signup' const CAPTCHA_PATH = '/gate/signup'
@ -61,6 +62,16 @@ export function StepCaptcha() {
[_, dispatch, state.handle], [_, dispatch, state.handle],
) )
const onBackPress = React.useCallback(() => {
logger.error('Signup Flow Error', {
errorMessage:
'User went back from captcha step. Possibly encountered an error.',
registrationHandle: state.handle,
})
dispatch({type: 'prev'})
}, [dispatch, state.handle])
return ( return (
<ScreenTransition> <ScreenTransition>
<View style={[a.gap_lg]}> <View style={[a.gap_lg]}>
@ -86,6 +97,11 @@ export function StepCaptcha() {
</View> </View>
<FormError error={state.error} /> <FormError error={state.error} />
</View> </View>
<BackNextButtons
hideNext
isLoading={state.isLoading}
onBackPress={onBackPress}
/>
</ScreenTransition> </ScreenTransition>
) )
} }

View File

@ -1,56 +1,96 @@
import React from 'react' import React, {useRef} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import { import {logEvent} from '#/lib/statsig/statsig'
createFullHandle, import {createFullHandle, validateHandle} from '#/lib/strings/handles'
IsValidHandle, import {useAgent} from '#/state/session'
validateHandle,
} from '#/lib/strings/handles'
import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {ScreenTransition} from '#/screens/Login/ScreenTransition'
import {useSignupContext} from '#/screens/Signup/state' import {useSignupContext, useSubmitSignup} from '#/screens/Signup/state'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {BackNextButtons} from './BackNextButtons'
export function StepHandle() { export function StepHandle() {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const {state, dispatch} = useSignupContext() const {state, dispatch} = useSignupContext()
const submit = useSubmitSignup({state, dispatch})
const agent = useAgent()
const handleValueRef = useRef<string>(state.handle)
const [draftValue, setDraftValue] = React.useState(state.handle)
const [validCheck, setValidCheck] = React.useState<IsValidHandle>({ const onNextPress = React.useCallback(async () => {
handleChars: false, const handle = handleValueRef.current.trim()
hyphenStartOrEnd: false,
frontLength: false,
totalLength: true,
overall: false,
})
useFocusEffect(
React.useCallback(() => {
setValidCheck(validateHandle(state.handle, state.userDomain))
}, [state.handle, state.userDomain]),
)
const onHandleChange = React.useCallback(
(value: string) => {
if (state.error) {
dispatch({type: 'setError', value: ''})
}
dispatch({ dispatch({
type: 'setHandle', type: 'setHandle',
value, value: handle,
}) })
},
[dispatch, state.error],
)
const newValidCheck = validateHandle(handle, state.userDomain)
if (!newValidCheck.overall) {
return
}
try {
dispatch({type: 'setIsLoading', value: true})
const res = await agent.resolveHandle({
handle: createFullHandle(handle, state.userDomain),
})
if (res.data.did) {
dispatch({
type: 'setError',
value: _(msg`That handle is already taken.`),
})
return
}
} catch (e) {
// Don't have to handle
} finally {
dispatch({type: 'setIsLoading', value: false})
}
logEvent('signup:nextPressed', {
activeStep: state.activeStep,
phoneVerificationRequired:
state.serviceDescription?.phoneVerificationRequired,
})
// phoneVerificationRequired is actually whether a captcha is required
if (!state.serviceDescription?.phoneVerificationRequired) {
submit()
return
}
dispatch({type: 'next'})
}, [
_,
dispatch,
state.activeStep,
state.serviceDescription?.phoneVerificationRequired,
state.userDomain,
submit,
agent,
])
const onBackPress = React.useCallback(() => {
const handle = handleValueRef.current.trim()
dispatch({
type: 'setHandle',
value: handle,
})
dispatch({type: 'prev'})
logEvent('signup:backPressed', {
activeStep: state.activeStep,
})
}, [dispatch, state.activeStep])
const validCheck = validateHandle(draftValue, state.userDomain)
return ( return (
<ScreenTransition> <ScreenTransition>
<View style={[a.gap_lg]}> <View style={[a.gap_lg]}>
@ -59,9 +99,17 @@ export function StepHandle() {
<TextField.Icon icon={At} /> <TextField.Icon icon={At} />
<TextField.Input <TextField.Input
testID="handleInput" testID="handleInput"
onChangeText={onHandleChange} onChangeText={val => {
if (state.error) {
dispatch({type: 'setError', value: ''})
}
// These need to always be in sync.
handleValueRef.current = val
setDraftValue(val)
}}
label={_(msg`Input your user handle`)} label={_(msg`Input your user handle`)}
defaultValue={state.handle} defaultValue={draftValue}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}
autoFocus autoFocus
@ -69,13 +117,16 @@ export function StepHandle() {
/> />
</TextField.Root> </TextField.Root>
</View> </View>
{draftValue !== '' && (
<Text style={[a.text_md]}> <Text style={[a.text_md]}>
<Trans>Your full handle will be</Trans>{' '} <Trans>Your full handle will be</Trans>{' '}
<Text style={[a.text_md, a.font_bold]}> <Text style={[a.text_md, a.font_bold]}>
@{createFullHandle(state.handle, state.userDomain)} @{createFullHandle(draftValue, state.userDomain)}
</Text> </Text>
</Text> </Text>
)}
{draftValue !== '' && (
<View <View
style={[ style={[
a.w_full, a.w_full,
@ -121,7 +172,14 @@ export function StepHandle() {
)} )}
</View> </View>
</View> </View>
)}
</View> </View>
<BackNextButtons
isLoading={state.isLoading}
isNextDisabled={!validCheck.overall}
onBackPress={onBackPress}
onNextPress={onNextPress}
/>
</ScreenTransition> </ScreenTransition>
) )
} }

View File

@ -1,8 +1,10 @@
import React from 'react' import React, {useRef} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import * as EmailValidator from 'email-validator'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {ScreenTransition} from '#/screens/Login/ScreenTransition'
import {is13, is18, useSignupContext} from '#/screens/Signup/state' import {is13, is18, useSignupContext} from '#/screens/Signup/state'
@ -16,6 +18,7 @@ import {Envelope_Stroke2_Corner0_Rounded as Envelope} from '#/components/icons/E
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {BackNextButtons} from '../BackNextButtons'
function sanitizeDate(date: Date): Date { function sanitizeDate(date: Date): Date {
if (!date || date.toString() === 'Invalid Date') { if (!date || date.toString() === 'Invalid Date') {
@ -28,13 +31,72 @@ function sanitizeDate(date: Date): Date {
} }
export function StepInfo({ export function StepInfo({
onPressBack,
isServerError,
refetchServer,
isLoadingStarterPack, isLoadingStarterPack,
}: { }: {
onPressBack: () => void
isServerError: boolean
refetchServer: () => void
isLoadingStarterPack: boolean isLoadingStarterPack: boolean
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const {state, dispatch} = useSignupContext() const {state, dispatch} = useSignupContext()
const inviteCodeValueRef = useRef<string>(state.inviteCode)
const emailValueRef = useRef<string>(state.email)
const passwordValueRef = useRef<string>(state.password)
const onNextPress = React.useCallback(async () => {
const inviteCode = inviteCodeValueRef.current
const email = emailValueRef.current
const password = passwordValueRef.current
if (!is13(state.dateOfBirth)) {
return
}
if (state.serviceDescription?.inviteCodeRequired && !inviteCode) {
return dispatch({
type: 'setError',
value: _(msg`Please enter your invite code.`),
})
}
if (!email) {
return dispatch({
type: 'setError',
value: _(msg`Please enter your email.`),
})
}
if (!EmailValidator.validate(email)) {
return dispatch({
type: 'setError',
value: _(msg`Your email appears to be invalid.`),
})
}
if (!password) {
return dispatch({
type: 'setError',
value: _(msg`Please choose your password.`),
})
}
dispatch({type: 'setInviteCode', value: inviteCode})
dispatch({type: 'setEmail', value: email})
dispatch({type: 'setPassword', value: password})
dispatch({type: 'next'})
logEvent('signup:nextPressed', {
activeStep: state.activeStep,
})
}, [
_,
dispatch,
state.activeStep,
state.dateOfBirth,
state.serviceDescription?.inviteCodeRequired,
])
return ( return (
<ScreenTransition> <ScreenTransition>
<View style={[a.gap_md]}> <View style={[a.gap_md]}>
@ -65,10 +127,7 @@ export function StepInfo({
<TextField.Icon icon={Ticket} /> <TextField.Icon icon={Ticket} />
<TextField.Input <TextField.Input
onChangeText={value => { onChangeText={value => {
dispatch({ inviteCodeValueRef.current = value.trim()
type: 'setInviteCode',
value: value.trim(),
})
}} }}
label={_(msg`Required for this provider`)} label={_(msg`Required for this provider`)}
defaultValue={state.inviteCode} defaultValue={state.inviteCode}
@ -88,10 +147,7 @@ export function StepInfo({
<TextField.Input <TextField.Input
testID="emailInput" testID="emailInput"
onChangeText={value => { onChangeText={value => {
dispatch({ emailValueRef.current = value.trim()
type: 'setEmail',
value: value.trim(),
})
}} }}
label={_(msg`Enter your email address`)} label={_(msg`Enter your email address`)}
defaultValue={state.email} defaultValue={state.email}
@ -110,10 +166,7 @@ export function StepInfo({
<TextField.Input <TextField.Input
testID="passwordInput" testID="passwordInput"
onChangeText={value => { onChangeText={value => {
dispatch({ passwordValueRef.current = value
type: 'setPassword',
value,
})
}} }}
label={_(msg`Choose your password`)} label={_(msg`Choose your password`)}
defaultValue={state.password} defaultValue={state.password}
@ -147,6 +200,14 @@ export function StepInfo({
</> </>
) : undefined} ) : undefined}
</View> </View>
<BackNextButtons
hideNext={!is13(state.dateOfBirth)}
showRetry={isServerError}
isLoading={state.isLoading}
onBackPress={onPressBack}
onNextPress={onNextPress}
onRetryPress={refetchServer}
/>
</ScreenTransition> </ScreenTransition>
) )
} }

View File

@ -7,11 +7,7 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics' import {useAnalytics} from '#/lib/analytics/analytics'
import {FEEDBACK_FORM_URL} from '#/lib/constants' import {FEEDBACK_FORM_URL} from '#/lib/constants'
import {logEvent} from '#/lib/statsig/statsig'
import {createFullHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {useServiceQuery} from '#/state/queries/service' import {useServiceQuery} from '#/state/queries/service'
import {useAgent} from '#/state/session'
import {useStarterPackQuery} from 'state/queries/starter-packs' import {useStarterPackQuery} from 'state/queries/starter-packs'
import {useActiveStarterPack} from 'state/shell/starter-pack' import {useActiveStarterPack} from 'state/shell/starter-pack'
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
@ -20,14 +16,12 @@ import {
reducer, reducer,
SignupContext, SignupContext,
SignupStep, SignupStep,
useSubmitSignup,
} from '#/screens/Signup/state' } from '#/screens/Signup/state'
import {StepCaptcha} from '#/screens/Signup/StepCaptcha' import {StepCaptcha} from '#/screens/Signup/StepCaptcha'
import {StepHandle} from '#/screens/Signup/StepHandle' import {StepHandle} from '#/screens/Signup/StepHandle'
import {StepInfo} from '#/screens/Signup/StepInfo' import {StepInfo} from '#/screens/Signup/StepInfo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider' import {Divider} from '#/components/Divider'
import {LinearGradientBackground} from '#/components/LinearGradientBackground' import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
@ -38,9 +32,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
const t = useTheme() const t = useTheme()
const {screen} = useAnalytics() const {screen} = useAnalytics()
const [state, dispatch] = React.useReducer(reducer, initialState) const [state, dispatch] = React.useReducer(reducer, initialState)
const submit = useSubmitSignup({state, dispatch})
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const agent = useAgent()
const activeStarterPack = useActiveStarterPack() const activeStarterPack = useActiveStarterPack()
const { const {
@ -89,72 +81,6 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
} }
}, [_, serviceInfo, isError]) }, [_, serviceInfo, isError])
const onNextPress = React.useCallback(async () => {
if (state.activeStep === SignupStep.HANDLE) {
try {
dispatch({type: 'setIsLoading', value: true})
const res = await agent.resolveHandle({
handle: createFullHandle(state.handle, state.userDomain),
})
if (res.data.did) {
dispatch({
type: 'setError',
value: _(msg`That handle is already taken.`),
})
return
}
} catch (e) {
// Don't have to handle
} finally {
dispatch({type: 'setIsLoading', value: false})
}
}
logEvent('signup:nextPressed', {
activeStep: state.activeStep,
phoneVerificationRequired:
state.serviceDescription?.phoneVerificationRequired,
})
// phoneVerificationRequired is actually whether a captcha is required
if (
state.activeStep === SignupStep.HANDLE &&
!state.serviceDescription?.phoneVerificationRequired
) {
submit()
return
}
dispatch({type: 'next'})
}, [
_,
state.activeStep,
state.handle,
state.serviceDescription?.phoneVerificationRequired,
state.userDomain,
submit,
agent,
])
const onBackPress = React.useCallback(() => {
if (state.activeStep !== SignupStep.INFO) {
if (state.activeStep === SignupStep.CAPTCHA) {
logger.error('Signup Flow Error', {
errorMessage:
'User went back from captcha step. Possibly encountered an error.',
registrationHandle: state.handle,
})
}
dispatch({type: 'prev'})
} else {
onPressBack()
}
logEvent('signup:backPressed', {
activeStep: state.activeStep,
})
}, [onPressBack, state.activeStep, state.handle])
return ( return (
<SignupContext.Provider value={{state, dispatch}}> <SignupContext.Provider value={{state, dispatch}}>
<LoggedOutLayout <LoggedOutLayout
@ -215,13 +141,15 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
</Text> </Text>
</View> </View>
<View style={[a.pb_3xl]}>
<LayoutAnimationConfig skipEntering skipExiting> <LayoutAnimationConfig skipEntering skipExiting>
{state.activeStep === SignupStep.INFO ? ( {state.activeStep === SignupStep.INFO ? (
<StepInfo <StepInfo
onPressBack={onPressBack}
isLoadingStarterPack={ isLoadingStarterPack={
isFetchingStarterPack && !isErrorStarterPack isFetchingStarterPack && !isErrorStarterPack
} }
isServerError={isError}
refetchServer={refetch}
/> />
) : state.activeStep === SignupStep.HANDLE ? ( ) : state.activeStep === SignupStep.HANDLE ? (
<StepHandle /> <StepHandle />
@ -229,50 +157,6 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
<StepCaptcha /> <StepCaptcha />
)} )}
</LayoutAnimationConfig> </LayoutAnimationConfig>
</View>
<View style={[a.flex_row, a.justify_between, a.pb_lg]}>
<Button
label={_(msg`Go back to previous step`)}
variant="solid"
color="secondary"
size="medium"
onPress={onBackPress}>
<ButtonText>
<Trans>Back</Trans>
</ButtonText>
</Button>
{state.activeStep !== SignupStep.CAPTCHA && (
<>
{isError ? (
<Button
label={_(msg`Press to retry`)}
variant="solid"
color="primary"
size="medium"
disabled={state.isLoading}
onPress={() => refetch()}>
<ButtonText>
<Trans>Retry</Trans>
</ButtonText>
</Button>
) : (
<Button
testID="nextBtn"
label={_(msg`Continue to next step`)}
variant="solid"
color="primary"
size="medium"
disabled={!state.canNext || state.isLoading}
onPress={onNextPress}>
<ButtonText>
<Trans>Next</Trans>
</ButtonText>
</Button>
)}
</>
)}
</View>
<Divider /> <Divider />

View File

@ -10,7 +10,7 @@ import * as EmailValidator from 'email-validator'
import {DEFAULT_SERVICE} from '#/lib/constants' import {DEFAULT_SERVICE} from '#/lib/constants'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {createFullHandle, validateHandle} from '#/lib/strings/handles' import {createFullHandle} from '#/lib/strings/handles'
import {getAge} from '#/lib/strings/time' import {getAge} from '#/lib/strings/time'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useSessionApi} from '#/state/session' import {useSessionApi} from '#/state/session'
@ -28,7 +28,6 @@ export enum SignupStep {
export type SignupState = { export type SignupState = {
hasPrev: boolean hasPrev: boolean
canNext: boolean
activeStep: SignupStep activeStep: SignupStep
serviceUrl: string serviceUrl: string
@ -58,12 +57,10 @@ export type SignupAction =
| {type: 'setHandle'; value: string} | {type: 'setHandle'; value: string}
| {type: 'setVerificationCode'; value: string} | {type: 'setVerificationCode'; value: string}
| {type: 'setError'; value: string} | {type: 'setError'; value: string}
| {type: 'setCanNext'; value: boolean}
| {type: 'setIsLoading'; value: boolean} | {type: 'setIsLoading'; value: boolean}
export const initialState: SignupState = { export const initialState: SignupState = {
hasPrev: false, hasPrev: false,
canNext: false,
activeStep: SignupStep.INFO, activeStep: SignupStep.INFO,
serviceUrl: DEFAULT_SERVICE, serviceUrl: DEFAULT_SERVICE,
@ -144,10 +141,6 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
next.handle = a.value next.handle = a.value
break break
} }
case 'setCanNext': {
next.canNext = a.value
break
}
case 'setIsLoading': { case 'setIsLoading': {
next.isLoading = a.value next.isLoading = a.value
break break
@ -160,23 +153,6 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
next.hasPrev = next.activeStep !== SignupStep.INFO next.hasPrev = next.activeStep !== SignupStep.INFO
switch (next.activeStep) {
case SignupStep.INFO: {
const isValidEmail = EmailValidator.validate(next.email)
next.canNext =
!!(next.email && next.password && next.dateOfBirth) &&
(!next.serviceDescription?.inviteCodeRequired || !!next.inviteCode) &&
is13(next.dateOfBirth) &&
isValidEmail
break
}
case SignupStep.HANDLE: {
next.canNext =
!!next.handle && validateHandle(next.handle, next.userDomain).overall
break
}
}
logger.debug('signup', next) logger.debug('signup', next)
if (s.activeStep !== next.activeStep) { if (s.activeStep !== next.activeStep) {