simplify handle validation during signup

This commit is contained in:
Hailey
2024-04-29 21:48:25 -07:00
parent 5d19f27052
commit 9cfbbe9512
4 changed files with 63 additions and 103 deletions
+22 -18
View File
@@ -25,28 +25,32 @@ export function sanitizeHandle(handle: string, prefix = ''): string {
return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}` return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}`
} }
export interface IsValidHandle { type ValidHandleResult =
handleChars: boolean | 'charsError'
hyphenStartOrEnd: boolean | 'hyphenError'
frontLength: boolean | 'frontLengthError'
totalLength: boolean | 'totalLengthError'
overall: boolean | 'valid'
}
// More checks from https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/index.ts#L72 // More checks from https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/index.ts#L72
export function validateHandle(str: string, userDomain: string): IsValidHandle { export function validateHandle(
str: string,
userDomain: string,
): ValidHandleResult {
const fullHandle = createFullHandle(str, userDomain) const fullHandle = createFullHandle(str, userDomain)
const results = { if (str.length < 3) {
handleChars: return 'frontLengthError'
!str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')), }
hyphenStartOrEnd: !str.startsWith('-') && !str.endsWith('-'), if (fullHandle.length > 253) {
frontLength: str.length >= 3, return 'totalLengthError'
totalLength: fullHandle.length <= 253, }
if (str.startsWith('-') || str.endsWith('-')) {
return 'hyphenError'
}
if (!str || !VALIDATE_REGEX.test(fullHandle) || str.includes('.')) {
return 'charsError'
} }
return { return 'valid'
...results,
overall: !Object.values(results).includes(false),
}
} }
+4 -78
View File
@@ -2,41 +2,20 @@ import React 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 {createFullHandle} from '#/lib/strings/handles'
createFullHandle,
IsValidHandle,
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} from '#/screens/Signup/state'
import {atoms as a, useTheme} from '#/alf' import {atoms as a} from '#/alf'
import {FormError} from '#/components/forms/FormError'
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 {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export function StepHandle() { export function StepHandle() {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme()
const {state, dispatch} = useSignupContext() const {state, dispatch} = useSignupContext()
const [validCheck, setValidCheck] = React.useState<IsValidHandle>({
handleChars: false,
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( const onHandleChange = React.useCallback(
(value: string) => { (value: string) => {
if (state.error) { if (state.error) {
@@ -75,61 +54,8 @@ export function StepHandle() {
@{createFullHandle(state.handle, state.userDomain)} @{createFullHandle(state.handle, state.userDomain)}
</Text> </Text>
</Text> </Text>
<FormError error={state.error} />
<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>
) : (
<Text style={[a.text_md, a.flex_1]}>
<Trans>At least 3 characters</Trans>
</Text>
)}
</View>
</View>
</View> </View>
</ScreenTransition> </ScreenTransition>
) )
} }
function IsValidIcon({valid}: {valid: boolean}) {
const t = useTheme()
if (!valid) {
return <Times size="md" style={{color: t.palette.negative_500}} />
}
return <Check size="md" style={{color: t.palette.positive_700}} />
}
+34 -1
View File
@@ -7,7 +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 {logEvent} from '#/lib/statsig/statsig'
import {createFullHandle} from '#/lib/strings/handles' import {createFullHandle, validateHandle} from '#/lib/strings/handles'
import {useServiceQuery} from '#/state/queries/service' import {useServiceQuery} from '#/state/queries/service'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
@@ -74,6 +74,39 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
const onNextPress = React.useCallback(async () => { const onNextPress = React.useCallback(async () => {
if (state.activeStep === SignupStep.HANDLE) { if (state.activeStep === SignupStep.HANDLE) {
try { try {
const isValidHandle = validateHandle(state.handle, state.userDomain)
switch (isValidHandle) {
case 'charsError':
dispatch({
type: 'setError',
value: _(
msg`Handle may only contain letters, numbers, and hyphens`,
),
})
return
case 'totalLengthError':
dispatch({
type: 'setError',
value: _(msg`Handle may not be longer than 253 characters`),
})
return
case 'frontLengthError':
dispatch({
type: 'setError',
value: _(msg`Handle should be at least 3 characters`),
})
return
case 'hyphenError':
dispatch({
type: 'setError',
value: _(msg`Handle may not begin or end with a hyphen`),
})
return
case 'valid':
break
}
dispatch({type: 'setIsLoading', value: true}) dispatch({type: 'setIsLoading', value: true})
const res = await getAgent().resolveHandle({ const res = await getAgent().resolveHandle({
+3 -6
View File
@@ -10,7 +10,7 @@ import * as EmailValidator from 'email-validator'
import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants' import {DEFAULT_SERVICE, IS_PROD_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 { import {
@@ -175,11 +175,8 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
isValidEmail isValidEmail
break break
} }
case SignupStep.HANDLE: { default:
next.canNext = next.canNext = true
!!next.handle && validateHandle(next.handle, next.userDomain).overall
break
}
} }
logger.debug('signup', next) logger.debug('signup', next)