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 t = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [isReady, setIsReady] = useState<boolean>(false)
const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
useState<boolean>(false)
const [identifier, setIdentifier] = useState<string>(initialHandle)
const [password, setPassword] = useState<string>('')
const [authFactorToken, setAuthFactorToken] = useState<string>('')
const passwordInputRef = useRef<TextInput>(null)
const identifierValueRef = useRef<string>(initialHandle || '')
const passwordValueRef = useRef<string>('')
const authFactorTokenValueRef = useRef<string>('')
const passwordRef = useRef<TextInput>(null)
const {_} = useLingui()
const {login} = useSessionApi()
const requestNotificationsPermission = useRequestNotificationsPermission()
@ -84,6 +85,10 @@ export const LoginForm = ({
setError('')
setIsProcessing(true)
const identifier = identifierValueRef.current.toLowerCase().trim()
const password = passwordValueRef.current
const authFactorToken = authFactorTokenValueRef.current
try {
// try to guess the handle if the user just gave their own username
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 (
<FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
<View>
@ -181,14 +201,15 @@ export const LoginForm = ({
autoComplete="username"
returnKeyType="next"
textContentType="username"
defaultValue={initialHandle || ''}
onChangeText={v => {
identifierValueRef.current = v
checkIsReady()
}}
onSubmitEditing={() => {
passwordInputRef.current?.focus()
passwordRef.current?.focus()
}}
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
value={identifier}
onChangeText={str =>
setIdentifier((str || '').toLowerCase().trim())
}
editable={!isProcessing}
accessibilityHint={_(
msg`Input the username or email address you used at signup`,
@ -200,7 +221,7 @@ export const LoginForm = ({
<TextField.Icon icon={Lock} />
<TextField.Input
testID="loginPasswordInput"
inputRef={passwordInputRef}
inputRef={passwordRef}
label={_(msg`Password`)}
autoCapitalize="none"
autoCorrect={false}
@ -210,16 +231,14 @@ export const LoginForm = ({
secureTextEntry={true}
textContentType="password"
clearButtonMode="while-editing"
value={password}
onChangeText={setPassword}
onChangeText={v => {
passwordValueRef.current = v
checkIsReady()
}}
onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
editable={!isProcessing}
accessibilityHint={
identifier === ''
? _(msg`Input your password`)
: _(msg`Input the password tied to ${identifier}`)
}
accessibilityHint={_(msg`Input your password`)}
/>
<Button
testID="forgotPasswordButton"
@ -258,8 +277,9 @@ export const LoginForm = ({
returnKeyType="done"
textContentType="username"
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
value={authFactorToken}
onChangeText={setAuthFactorToken}
onChangeText={v => {
authFactorTokenValueRef.current = v
}}
onSubmitEditing={onPressNext}
editable={!isProcessing}
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 {atoms as a, useTheme} from '#/alf'
import {FormError} from '#/components/forms/FormError'
import {BackNextButtons} from '../BackNextButtons'
const CAPTCHA_PATH = '/gate/signup'
@ -61,6 +62,16 @@ export function StepCaptcha() {
[_, 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 (
<ScreenTransition>
<View style={[a.gap_lg]}>
@ -86,6 +97,11 @@ export function StepCaptcha() {
</View>
<FormError error={state.error} />
</View>
<BackNextButtons
hideNext
isLoading={state.isLoading}
onBackPress={onBackPress}
/>
</ScreenTransition>
)
}

View File

@ -1,56 +1,96 @@
import React from 'react'
import React, {useRef} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {
createFullHandle,
IsValidHandle,
validateHandle,
} from '#/lib/strings/handles'
import {logEvent} from '#/lib/statsig/statsig'
import {createFullHandle, validateHandle} from '#/lib/strings/handles'
import {useAgent} from '#/state/session'
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 * as TextField from '#/components/forms/TextField'
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Text} from '#/components/Typography'
import {BackNextButtons} from './BackNextButtons'
export function StepHandle() {
const {_} = useLingui()
const t = useTheme()
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>({
handleChars: false,
hyphenStartOrEnd: false,
frontLength: false,
totalLength: true,
overall: false,
})
const onNextPress = React.useCallback(async () => {
const handle = handleValueRef.current.trim()
dispatch({
type: 'setHandle',
value: handle,
})
useFocusEffect(
React.useCallback(() => {
setValidCheck(validateHandle(state.handle, state.userDomain))
}, [state.handle, state.userDomain]),
)
const newValidCheck = validateHandle(handle, state.userDomain)
if (!newValidCheck.overall) {
return
}
const onHandleChange = React.useCallback(
(value: string) => {
if (state.error) {
dispatch({type: 'setError', value: ''})
}
try {
dispatch({type: 'setIsLoading', value: true})
dispatch({
type: 'setHandle',
value,
const res = await agent.resolveHandle({
handle: createFullHandle(handle, state.userDomain),
})
},
[dispatch, state.error],
)
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 (
<ScreenTransition>
<View style={[a.gap_lg]}>
@ -59,9 +99,17 @@ export function StepHandle() {
<TextField.Icon icon={At} />
<TextField.Input
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`)}
defaultValue={state.handle}
defaultValue={draftValue}
autoCapitalize="none"
autoCorrect={false}
autoFocus
@ -69,59 +117,69 @@ export function StepHandle() {
/>
</TextField.Root>
</View>
<Text style={[a.text_md]}>
<Trans>Your full handle will be</Trans>{' '}
<Text style={[a.text_md, a.font_bold]}>
@{createFullHandle(state.handle, state.userDomain)}
{draftValue !== '' && (
<Text style={[a.text_md]}>
<Trans>Your full handle will be</Trans>{' '}
<Text style={[a.text_md, a.font_bold]}>
@{createFullHandle(draftValue, state.userDomain)}
</Text>
</Text>
</Text>
)}
<View
style={[
a.w_full,
a.rounded_sm,
a.border,
a.p_md,
a.gap_sm,
t.atoms.border_contrast_low,
]}>
{state.error ? (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={false} />
<Text style={[a.text_md, a.flex_1]}>{state.error}</Text>
</View>
) : undefined}
{validCheck.hyphenStartOrEnd ? (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={validCheck.handleChars} />
<Text style={[a.text_md, a.flex_1]}>
<Trans>Only contains letters, numbers, and hyphens</Trans>
</Text>
</View>
) : (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={validCheck.hyphenStartOrEnd} />
<Text style={[a.text_md, a.flex_1]}>
<Trans>Doesn't begin or end with a hyphen</Trans>
</Text>
</View>
)}
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon
valid={validCheck.frontLength && validCheck.totalLength}
/>
{!validCheck.totalLength ? (
<Text style={[a.text_md, a.flex_1]}>
<Trans>No longer than 253 characters</Trans>
</Text>
{draftValue !== '' && (
<View
style={[
a.w_full,
a.rounded_sm,
a.border,
a.p_md,
a.gap_sm,
t.atoms.border_contrast_low,
]}>
{state.error ? (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={false} />
<Text style={[a.text_md, a.flex_1]}>{state.error}</Text>
</View>
) : undefined}
{validCheck.hyphenStartOrEnd ? (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={validCheck.handleChars} />
<Text style={[a.text_md, a.flex_1]}>
<Trans>Only contains letters, numbers, and hyphens</Trans>
</Text>
</View>
) : (
<Text style={[a.text_md, a.flex_1]}>
<Trans>At least 3 characters</Trans>
</Text>
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon valid={validCheck.hyphenStartOrEnd} />
<Text style={[a.text_md, a.flex_1]}>
<Trans>Doesn't begin or end with a hyphen</Trans>
</Text>
</View>
)}
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidIcon
valid={validCheck.frontLength && validCheck.totalLength}
/>
{!validCheck.totalLength ? (
<Text style={[a.text_md, a.flex_1]}>
<Trans>No longer than 253 characters</Trans>
</Text>
) : (
<Text style={[a.text_md, a.flex_1]}>
<Trans>At least 3 characters</Trans>
</Text>
)}
</View>
</View>
</View>
)}
</View>
<BackNextButtons
isLoading={state.isLoading}
isNextDisabled={!validCheck.overall}
onBackPress={onBackPress}
onNextPress={onNextPress}
/>
</ScreenTransition>
)
}

View File

@ -1,8 +1,10 @@
import React from 'react'
import React, {useRef} from 'react'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as EmailValidator from 'email-validator'
import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger'
import {ScreenTransition} from '#/screens/Login/ScreenTransition'
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 {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
import {Loader} from '#/components/Loader'
import {BackNextButtons} from '../BackNextButtons'
function sanitizeDate(date: Date): Date {
if (!date || date.toString() === 'Invalid Date') {
@ -28,13 +31,72 @@ function sanitizeDate(date: Date): Date {
}
export function StepInfo({
onPressBack,
isServerError,
refetchServer,
isLoadingStarterPack,
}: {
onPressBack: () => void
isServerError: boolean
refetchServer: () => void
isLoadingStarterPack: boolean
}) {
const {_} = useLingui()
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 (
<ScreenTransition>
<View style={[a.gap_md]}>
@ -65,10 +127,7 @@ export function StepInfo({
<TextField.Icon icon={Ticket} />
<TextField.Input
onChangeText={value => {
dispatch({
type: 'setInviteCode',
value: value.trim(),
})
inviteCodeValueRef.current = value.trim()
}}
label={_(msg`Required for this provider`)}
defaultValue={state.inviteCode}
@ -88,10 +147,7 @@ export function StepInfo({
<TextField.Input
testID="emailInput"
onChangeText={value => {
dispatch({
type: 'setEmail',
value: value.trim(),
})
emailValueRef.current = value.trim()
}}
label={_(msg`Enter your email address`)}
defaultValue={state.email}
@ -110,10 +166,7 @@ export function StepInfo({
<TextField.Input
testID="passwordInput"
onChangeText={value => {
dispatch({
type: 'setPassword',
value,
})
passwordValueRef.current = value
}}
label={_(msg`Choose your password`)}
defaultValue={state.password}
@ -147,6 +200,14 @@ export function StepInfo({
</>
) : undefined}
</View>
<BackNextButtons
hideNext={!is13(state.dateOfBirth)}
showRetry={isServerError}
isLoading={state.isLoading}
onBackPress={onPressBack}
onNextPress={onNextPress}
onRetryPress={refetchServer}
/>
</ScreenTransition>
)
}

View File

@ -7,11 +7,7 @@ import {useLingui} from '@lingui/react'
import {useAnalytics} from '#/lib/analytics/analytics'
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 {useAgent} from '#/state/session'
import {useStarterPackQuery} from 'state/queries/starter-packs'
import {useActiveStarterPack} from 'state/shell/starter-pack'
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
@ -20,14 +16,12 @@ import {
reducer,
SignupContext,
SignupStep,
useSubmitSignup,
} from '#/screens/Signup/state'
import {StepCaptcha} from '#/screens/Signup/StepCaptcha'
import {StepHandle} from '#/screens/Signup/StepHandle'
import {StepInfo} from '#/screens/Signup/StepInfo'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
import {Button, ButtonText} from '#/components/Button'
import {Divider} from '#/components/Divider'
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {InlineLinkText} from '#/components/Link'
@ -38,9 +32,7 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
const t = useTheme()
const {screen} = useAnalytics()
const [state, dispatch] = React.useReducer(reducer, initialState)
const submit = useSubmitSignup({state, dispatch})
const {gtMobile} = useBreakpoints()
const agent = useAgent()
const activeStarterPack = useActiveStarterPack()
const {
@ -89,72 +81,6 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
}
}, [_, 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 (
<SignupContext.Provider value={{state, dispatch}}>
<LoggedOutLayout
@ -215,64 +141,22 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
</Text>
</View>
<View style={[a.pb_3xl]}>
<LayoutAnimationConfig skipEntering skipExiting>
{state.activeStep === SignupStep.INFO ? (
<StepInfo
isLoadingStarterPack={
isFetchingStarterPack && !isErrorStarterPack
}
/>
) : state.activeStep === SignupStep.HANDLE ? (
<StepHandle />
) : (
<StepCaptcha />
)}
</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>
)}
</>
<LayoutAnimationConfig skipEntering skipExiting>
{state.activeStep === SignupStep.INFO ? (
<StepInfo
onPressBack={onPressBack}
isLoadingStarterPack={
isFetchingStarterPack && !isErrorStarterPack
}
isServerError={isError}
refetchServer={refetch}
/>
) : state.activeStep === SignupStep.HANDLE ? (
<StepHandle />
) : (
<StepCaptcha />
)}
</View>
</LayoutAnimationConfig>
<Divider />

View File

@ -10,7 +10,7 @@ import * as EmailValidator from 'email-validator'
import {DEFAULT_SERVICE} from '#/lib/constants'
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 {logger} from '#/logger'
import {useSessionApi} from '#/state/session'
@ -28,7 +28,6 @@ export enum SignupStep {
export type SignupState = {
hasPrev: boolean
canNext: boolean
activeStep: SignupStep
serviceUrl: string
@ -58,12 +57,10 @@ export type SignupAction =
| {type: 'setHandle'; value: string}
| {type: 'setVerificationCode'; value: string}
| {type: 'setError'; value: string}
| {type: 'setCanNext'; value: boolean}
| {type: 'setIsLoading'; value: boolean}
export const initialState: SignupState = {
hasPrev: false,
canNext: false,
activeStep: SignupStep.INFO,
serviceUrl: DEFAULT_SERVICE,
@ -144,10 +141,6 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
next.handle = a.value
break
}
case 'setCanNext': {
next.canNext = a.value
break
}
case 'setIsLoading': {
next.isLoading = a.value
break
@ -160,23 +153,6 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
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)
if (s.activeStep !== next.activeStep) {