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}`
|
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),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}} />
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user