Basic minimum password requirements, plus field-specific errors (#7811)

* add min password requirement

* add field specific errors

* move email tld check to after other email checks

* add password length check to change password dialog

* Update src/view/com/modals/ChangePassword.tsx

Co-authored-by: Hailey <me@haileyok.com>

* Update src/screens/Signup/StepInfo/index.tsx

Co-authored-by: Hailey <me@haileyok.com>

* fix lint

---------

Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
Samuel Newman
2025-02-24 12:44:21 -08:00
committed by GitHub
parent 3d954a00e0
commit ae9176c9c2
3 changed files with 79 additions and 23 deletions
+46 -19
View File
@@ -75,22 +75,6 @@ export function StepInfo({
const emailChanged = prevEmailValueRef.current !== email const emailChanged = prevEmailValueRef.current !== email
const password = passwordValueRef.current const password = passwordValueRef.current
if (emailChanged && tldtsRef.current) {
if (isEmailMaybeInvalid(email, tldtsRef.current)) {
prevEmailValueRef.current = email
setHasWarnedEmail(true)
return dispatch({
type: 'setError',
value: _(
msg`It looks like you may have entered your email address incorrectly. Are you sure it's right?`,
),
})
}
} else if (hasWarnedEmail) {
setHasWarnedEmail(false)
}
prevEmailValueRef.current = email
if (!is13(state.dateOfBirth)) { if (!is13(state.dateOfBirth)) {
return return
} }
@@ -99,24 +83,50 @@ export function StepInfo({
return dispatch({ return dispatch({
type: 'setError', type: 'setError',
value: _(msg`Please enter your invite code.`), value: _(msg`Please enter your invite code.`),
field: 'invite-code',
}) })
} }
if (!email) { if (!email) {
return dispatch({ return dispatch({
type: 'setError', type: 'setError',
value: _(msg`Please enter your email.`), value: _(msg`Please enter your email.`),
field: 'email',
}) })
} }
if (!EmailValidator.validate(email)) { if (!EmailValidator.validate(email)) {
return dispatch({ return dispatch({
type: 'setError', type: 'setError',
value: _(msg`Your email appears to be invalid.`), value: _(msg`Your email appears to be invalid.`),
field: 'email',
}) })
} }
if (emailChanged && tldtsRef.current) {
if (isEmailMaybeInvalid(email, tldtsRef.current)) {
prevEmailValueRef.current = email
setHasWarnedEmail(true)
return dispatch({
type: 'setError',
value: _(
msg`Please double-check that you have entered your email address correctly.`,
),
})
}
} else if (hasWarnedEmail) {
setHasWarnedEmail(false)
}
prevEmailValueRef.current = email
if (!password) { if (!password) {
return dispatch({ return dispatch({
type: 'setError', type: 'setError',
value: _(msg`Please choose your password.`), value: _(msg`Please choose your password.`),
field: 'password',
})
}
if (password.length < 8) {
return dispatch({
type: 'setError',
value: _(msg`Your password must be at least 8 characters long.`),
field: 'password',
}) })
} }
@@ -149,11 +159,17 @@ export function StepInfo({
<TextField.LabelText> <TextField.LabelText>
<Trans>Invite code</Trans> <Trans>Invite code</Trans>
</TextField.LabelText> </TextField.LabelText>
<TextField.Root> <TextField.Root isInvalid={state.errorField === 'invite-code'}>
<TextField.Icon icon={Ticket} /> <TextField.Icon icon={Ticket} />
<TextField.Input <TextField.Input
onChangeText={value => { onChangeText={value => {
inviteCodeValueRef.current = value.trim() inviteCodeValueRef.current = value.trim()
if (
state.errorField === 'invite-code' &&
value.trim().length > 0
) {
dispatch({type: 'clearError'})
}
}} }}
label={_(msg`Required for this provider`)} label={_(msg`Required for this provider`)}
defaultValue={state.inviteCode} defaultValue={state.inviteCode}
@@ -173,7 +189,7 @@ export function StepInfo({
<TextField.LabelText> <TextField.LabelText>
<Trans>Email</Trans> <Trans>Email</Trans>
</TextField.LabelText> </TextField.LabelText>
<TextField.Root> <TextField.Root isInvalid={state.errorField === 'email'}>
<TextField.Icon icon={Envelope} /> <TextField.Icon icon={Envelope} />
<TextField.Input <TextField.Input
testID="emailInput" testID="emailInput"
@@ -183,6 +199,13 @@ export function StepInfo({
if (hasWarnedEmail) { if (hasWarnedEmail) {
setHasWarnedEmail(false) setHasWarnedEmail(false)
} }
if (
state.errorField === 'email' &&
value.trim().length > 0 &&
EmailValidator.validate(value.trim())
) {
dispatch({type: 'clearError'})
}
}} }}
label={_(msg`Enter your email address`)} label={_(msg`Enter your email address`)}
defaultValue={state.email} defaultValue={state.email}
@@ -201,13 +224,16 @@ export function StepInfo({
<TextField.LabelText> <TextField.LabelText>
<Trans>Password</Trans> <Trans>Password</Trans>
</TextField.LabelText> </TextField.LabelText>
<TextField.Root> <TextField.Root isInvalid={state.errorField === 'password'}>
<TextField.Icon icon={Lock} /> <TextField.Icon icon={Lock} />
<TextField.Input <TextField.Input
testID="passwordInput" testID="passwordInput"
inputRef={passwordInputRef} inputRef={passwordInputRef}
onChangeText={value => { onChangeText={value => {
passwordValueRef.current = value passwordValueRef.current = value
if (state.errorField === 'password' && value.length >= 8) {
dispatch({type: 'clearError'})
}
}} }}
label={_(msg`Choose your password`)} label={_(msg`Choose your password`)}
defaultValue={state.password} defaultValue={state.password}
@@ -219,6 +245,7 @@ export function StepInfo({
onSubmitEditing={native(() => onSubmitEditing={native(() =>
birthdateInputRef.current?.focus(), birthdateInputRef.current?.focus(),
)} )}
passwordRules="minlength: 8;"
/> />
</TextField.Root> </TextField.Root>
</View> </View>
+19 -1
View File
@@ -31,6 +31,13 @@ type SubmitTask = {
mutableProcessed: boolean // OK to mutate assuming it's never read in render. mutableProcessed: boolean // OK to mutate assuming it's never read in render.
} }
type ErrorField =
| 'invite-code'
| 'email'
| 'handle'
| 'password'
| 'date-of-birth'
export type SignupState = { export type SignupState = {
hasPrev: boolean hasPrev: boolean
activeStep: SignupStep activeStep: SignupStep
@@ -45,6 +52,7 @@ export type SignupState = {
handle: string handle: string
error: string error: string
errorField?: ErrorField
isLoading: boolean isLoading: boolean
pendingSubmit: null | SubmitTask pendingSubmit: null | SubmitTask
@@ -62,7 +70,8 @@ export type SignupAction =
| {type: 'setDateOfBirth'; value: Date} | {type: 'setDateOfBirth'; value: Date}
| {type: 'setInviteCode'; value: string} | {type: 'setInviteCode'; value: string}
| {type: 'setHandle'; value: string} | {type: 'setHandle'; value: string}
| {type: 'setError'; value: string} | {type: 'setError'; value: string; field?: ErrorField}
| {type: 'clearError'}
| {type: 'setIsLoading'; value: boolean} | {type: 'setIsLoading'; value: boolean}
| {type: 'submit'; task: SubmitTask} | {type: 'submit'; task: SubmitTask}
@@ -80,6 +89,7 @@ export const initialState: SignupState = {
inviteCode: '', inviteCode: '',
error: '', error: '',
errorField: undefined,
isLoading: false, isLoading: false,
pendingSubmit: null, pendingSubmit: null,
@@ -102,6 +112,7 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
next.activeStep-- next.activeStep--
next.error = '' next.error = ''
next.errorField = undefined
} }
break break
} }
@@ -110,6 +121,7 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
next.activeStep++ next.activeStep++
next.error = '' next.error = ''
next.errorField = undefined
} }
break break
} }
@@ -156,6 +168,12 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
} }
case 'setError': { case 'setError': {
next.error = a.value next.error = a.value
next.errorField = a.field
break
}
case 'clearError': {
next.error = ''
next.errorField = undefined
break break
} }
case 'submit': { case 'submit': {
+14 -3
View File
@@ -81,8 +81,7 @@ export function Component() {
const onChangePassword = async () => { const onChangePassword = async () => {
const formattedCode = checkAndFormatResetCode(resetCode) const formattedCode = checkAndFormatResetCode(resetCode)
// TODO Better password strength check if (!formattedCode) {
if (!formattedCode || !newPassword) {
setError( setError(
_( _(
msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
@@ -90,6 +89,16 @@ export function Component() {
) )
return return
} }
if (!newPassword) {
setError(
_(msg`Please enter a password. It must be at least 8 characters long.`),
)
return
}
if (newPassword.length < 8) {
setError(_(msg`Password must be at least 8 characters long.`))
return
}
setError('') setError('')
setIsProcessing(true) setIsProcessing(true)
@@ -104,7 +113,9 @@ export function Component() {
logger.warn('Failed to set new password', {error: e}) logger.warn('Failed to set new password', {error: e})
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(
'Unable to contact your service. Please check your Internet connection.', _(
msg`Unable to contact your service. Please check your Internet connection.`,
),
) )
} else { } else {
setError(cleanError(errMsg)) setError(cleanError(errMsg))