diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 7cfd38e34f..35b124b611 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -60,12 +60,13 @@ export const LoginForm = ({ const {track} = useAnalytics() const t = useTheme() const [isProcessing, setIsProcessing] = useState(false) + const [isReady, setIsReady] = useState(false) const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] = useState(false) - const [identifier, setIdentifier] = useState(initialHandle) - const [password, setPassword] = useState('') - const [authFactorToken, setAuthFactorToken] = useState('') - const passwordInputRef = useRef(null) + const identifierValueRef = useRef(initialHandle || '') + const passwordValueRef = useRef('') + const authFactorTokenValueRef = useRef('') + const passwordRef = useRef(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 ( Sign in}> @@ -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 = ({ { + 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`)} /> + {!hideNext && + (showRetry ? ( + + ) : ( + + ))} + + ) +} diff --git a/src/screens/Signup/StepCaptcha/index.tsx b/src/screens/Signup/StepCaptcha/index.tsx index b2a91a641c..bf35764908 100644 --- a/src/screens/Signup/StepCaptcha/index.tsx +++ b/src/screens/Signup/StepCaptcha/index.tsx @@ -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 ( @@ -86,6 +97,11 @@ export function StepCaptcha() { + ) } diff --git a/src/screens/Signup/StepHandle.tsx b/src/screens/Signup/StepHandle.tsx index 2266f43879..b443e822a4 100644 --- a/src/screens/Signup/StepHandle.tsx +++ b/src/screens/Signup/StepHandle.tsx @@ -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(state.handle) + const [draftValue, setDraftValue] = React.useState(state.handle) - const [validCheck, setValidCheck] = React.useState({ - 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 ( @@ -59,9 +99,17 @@ export function StepHandle() { { + 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() { /> - - Your full handle will be{' '} - - @{createFullHandle(state.handle, state.userDomain)} + {draftValue !== '' && ( + + Your full handle will be{' '} + + @{createFullHandle(draftValue, state.userDomain)} + - + )} - - {state.error ? ( - - - {state.error} - - ) : undefined} - {validCheck.hyphenStartOrEnd ? ( - - - - Only contains letters, numbers, and hyphens - - - ) : ( - - - - Doesn't begin or end with a hyphen - - - )} - - - {!validCheck.totalLength ? ( - - No longer than 253 characters - + {draftValue !== '' && ( + + {state.error ? ( + + + {state.error} + + ) : undefined} + {validCheck.hyphenStartOrEnd ? ( + + + + Only contains letters, numbers, and hyphens + + ) : ( - - At least 3 characters - + + + + Doesn't begin or end with a hyphen + + )} + + + {!validCheck.totalLength ? ( + + No longer than 253 characters + + ) : ( + + At least 3 characters + + )} + - + )} + ) } diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx index 691e23a537..47fb4c70ba 100644 --- a/src/screens/Signup/StepInfo/index.tsx +++ b/src/screens/Signup/StepInfo/index.tsx @@ -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(state.inviteCode) + const emailValueRef = useRef(state.email) + const passwordValueRef = useRef(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 ( @@ -65,10 +127,7 @@ export function StepInfo({ { - 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({ { - 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({ { - dispatch({ - type: 'setPassword', - value, - }) + passwordValueRef.current = value }} label={_(msg`Choose your password`)} defaultValue={state.password} @@ -147,6 +200,14 @@ export function StepInfo({ ) : undefined} + ) } diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx index f7ca180bff..da0383884b 100644 --- a/src/screens/Signup/index.tsx +++ b/src/screens/Signup/index.tsx @@ -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 ( void}) { - - - {state.activeStep === SignupStep.INFO ? ( - - ) : state.activeStep === SignupStep.HANDLE ? ( - - ) : ( - - )} - - - - - - {state.activeStep !== SignupStep.CAPTCHA && ( - <> - {isError ? ( - - ) : ( - - )} - + + {state.activeStep === SignupStep.INFO ? ( + + ) : state.activeStep === SignupStep.HANDLE ? ( + + ) : ( + )} - + diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts index 87700cb88e..826cbf1d31 100644 --- a/src/screens/Signup/state.ts +++ b/src/screens/Signup/state.ts @@ -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) {