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
parent
4bb4452f08
commit
63bb8fda2d
|
@ -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={_(
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue