simplify handle validation during signup
This commit is contained in:
+22
-18
@@ -25,28 +25,32 @@ export function sanitizeHandle(handle: string, prefix = ''): string {
|
||||
return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}`
|
||||
}
|
||||
|
||||
export interface IsValidHandle {
|
||||
handleChars: boolean
|
||||
hyphenStartOrEnd: boolean
|
||||
frontLength: boolean
|
||||
totalLength: boolean
|
||||
overall: boolean
|
||||
}
|
||||
type ValidHandleResult =
|
||||
| 'charsError'
|
||||
| 'hyphenError'
|
||||
| 'frontLengthError'
|
||||
| 'totalLengthError'
|
||||
| 'valid'
|
||||
|
||||
// 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 results = {
|
||||
handleChars:
|
||||
!str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')),
|
||||
hyphenStartOrEnd: !str.startsWith('-') && !str.endsWith('-'),
|
||||
frontLength: str.length >= 3,
|
||||
totalLength: fullHandle.length <= 253,
|
||||
if (str.length < 3) {
|
||||
return 'frontLengthError'
|
||||
}
|
||||
if (fullHandle.length > 253) {
|
||||
return 'totalLengthError'
|
||||
}
|
||||
if (str.startsWith('-') || str.endsWith('-')) {
|
||||
return 'hyphenError'
|
||||
}
|
||||
if (!str || !VALIDATE_REGEX.test(fullHandle) || str.includes('.')) {
|
||||
return 'charsError'
|
||||
}
|
||||
|
||||
return {
|
||||
...results,
|
||||
overall: !Object.values(results).includes(false),
|
||||
}
|
||||
return 'valid'
|
||||
}
|
||||
|
||||
@@ -2,41 +2,20 @@ import React 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 {createFullHandle} from '#/lib/strings/handles'
|
||||
import {ScreenTransition} from '#/screens/Login/ScreenTransition'
|
||||
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 {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'
|
||||
|
||||
export function StepHandle() {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
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(
|
||||
(value: string) => {
|
||||
if (state.error) {
|
||||
@@ -75,61 +54,8 @@ export function StepHandle() {
|
||||
@{createFullHandle(state.handle, state.userDomain)}
|
||||
</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>
|
||||
) : (
|
||||
<Text style={[a.text_md, a.flex_1]}>
|
||||
<Trans>At least 3 characters</Trans>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<FormError error={state.error} />
|
||||
</View>
|
||||
</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}} />
|
||||
}
|
||||
|
||||
@@ -7,7 +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 {createFullHandle, validateHandle} from '#/lib/strings/handles'
|
||||
import {useServiceQuery} from '#/state/queries/service'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
|
||||
@@ -74,6 +74,39 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
const onNextPress = React.useCallback(async () => {
|
||||
if (state.activeStep === SignupStep.HANDLE) {
|
||||
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})
|
||||
|
||||
const res = await getAgent().resolveHandle({
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as EmailValidator from 'email-validator'
|
||||
|
||||
import {DEFAULT_SERVICE, IS_PROD_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 {
|
||||
@@ -175,11 +175,8 @@ export function reducer(s: SignupState, a: SignupAction): SignupState {
|
||||
isValidEmail
|
||||
break
|
||||
}
|
||||
case SignupStep.HANDLE: {
|
||||
next.canNext =
|
||||
!!next.handle && validateHandle(next.handle, next.userDomain).overall
|
||||
break
|
||||
}
|
||||
default:
|
||||
next.canNext = true
|
||||
}
|
||||
|
||||
logger.debug('signup', next)
|
||||
|
||||
Reference in New Issue
Block a user