Skip to content

Commit

Permalink
show uiState errors in the box as well
Browse files Browse the repository at this point in the history
simplify copy

update ui for only letters and numbers

add ui validation to handle selection
  • Loading branch information
haileyok committed Feb 22, 2024
1 parent 874489b commit 817966a
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 34 deletions.
29 changes: 29 additions & 0 deletions src/lib/strings/handles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
// Regex from the go implementation
// https://github.com/bluesky-social/indigo/blob/main/atproto/syntax/handle.go#L10
const VALIDATE_REGEX =
/^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/

export function makeValidHandle(str: string): string {
if (str.length > 20) {
str = str.slice(0, 20)
Expand All @@ -19,3 +24,27 @@ export function isInvalidHandle(handle: string): boolean {
export function sanitizeHandle(handle: string, prefix = ''): string {
return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}`
}

export interface IsValidHandle {
handleChars: boolean
frontLength: boolean
totalLength: boolean
overall: boolean
}

// 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 {
const fullHandle = createFullHandle(str, userDomain)

const results = {
handleChars:
!str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')),
frontLength: str.length >= 3,
totalLength: fullHandle.length <= 253,
}

return {
...results,
overall: !Object.values(results).includes(false),
}
}
6 changes: 5 additions & 1 deletion src/view/com/auth/create/CreateAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {Step3} from './Step3'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {TextLink} from '../../util/Link'
import {getAgent} from 'state/session'
import {createFullHandle} from 'lib/strings/handles'
import {createFullHandle, validateHandle} from 'lib/strings/handles'

export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
const {screen} = useAnalytics()
Expand Down Expand Up @@ -78,6 +78,10 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
}

if (uiState.step === 2) {
if (!validateHandle(uiState.handle, uiState.userDomain).overall) {
return
}

uiDispatch({type: 'set-processing', value: true})
try {
const res = await getAgent().resolveHandle({
Expand Down
149 changes: 118 additions & 31 deletions src/view/com/auth/create/Step2.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {View} from 'react-native'
import {CreateAccountState, CreateAccountDispatch} from './state'
import {Text} from 'view/com/util/text/Text'
import {StepHeader} from './StepHeader'
import {s} from 'lib/styles'
import {TextInput} from '../util/TextInput'
import {createFullHandle} from 'lib/strings/handles'
import {
createFullHandle,
IsValidHandle,
validateHandle,
} from 'lib/strings/handles'
import {usePalette} from 'lib/hooks/usePalette'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {atoms as a, useTheme} from '#/alf'
import {Check_Stroke2_Corner0_Rounded} from '#/components/icons/Check'
import {TimesLarge_Stroke2_Corner0_Rounded} from '#/components/icons/Times'
import {useFocusEffect} from '@react-navigation/native'

/** STEP 3: Your user handle
* @field User handle
Expand All @@ -23,41 +30,121 @@ export function Step2({
}) {
const pal = usePalette('default')
const {_} = useLingui()
const t = useTheme()

const [validCheck, setValidCheck] = React.useState<IsValidHandle>({
handleChars: false,
frontLength: false,
totalLength: true,
overall: false,
})

useFocusEffect(
React.useCallback(() => {
setValidCheck(validateHandle(uiState.handle, uiState.userDomain))

// Disabling this, because we only want to run this when we focus the screen
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []),
)

const onHandleChange = React.useCallback(
(value: string) => {
if (uiState.error) {
uiDispatch({type: 'set-error', value: ''})
}

setValidCheck(validateHandle(value, uiState.userDomain))
uiDispatch({type: 'set-handle', value})
},
[uiDispatch, uiState.error, uiState.userDomain],
)

return (
<View>
<StepHeader uiState={uiState} title={_(msg`Your user handle`)} />
{uiState.error ? (
<ErrorMessage message={uiState.error} style={styles.error} />
) : undefined}
<View style={s.pb10}>
<TextInput
testID="handleInput"
icon="at"
placeholder="e.g. alice"
value={uiState.handle}
editable
autoFocus
autoComplete="off"
autoCorrect={false}
onChange={value => uiDispatch({type: 'set-handle', value})}
// TODO: Add explicit text label
accessibilityLabel={_(msg`User handle`)}
accessibilityHint={_(msg`Input your user handle`)}
/>
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
<Trans>Your full handle will be</Trans>{' '}
<Text type="lg-bold" style={pal.text}>
@{createFullHandle(uiState.handle, uiState.userDomain)}
<View style={s.mb20}>
<TextInput
testID="handleInput"
icon="at"
placeholder="e.g. alice"
value={uiState.handle}
editable
autoFocus
autoComplete="off"
autoCorrect={false}
onChange={onHandleChange}
// TODO: Add explicit text label
accessibilityLabel={_(msg`User handle`)}
accessibilityHint={_(msg`Input your user handle`)}
/>
<Text type="lg" style={[pal.text, s.pl5, s.pt10]}>
<Trans>Your full handle will be</Trans>{' '}
<Text type="lg-bold" style={pal.text}>
@{createFullHandle(uiState.handle, uiState.userDomain)}
</Text>
</Text>
</Text>
</View>
<View
style={[
a.w_full,
a.rounded_sm,
a.border,
a.p_md,
a.gap_sm,
t.atoms.border_contrast_low,
]}>
{uiState.error && (
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidCheck valid={false} />
<Text style={[t.atoms.text, a.text_md, a.flex]}>
{uiState.error}
</Text>
</View>
)}
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidCheck valid={validCheck.handleChars} />
<Text style={[t.atoms.text, a.text_md, a.flex]}>
<Trans>May only contain letters and numbers</Trans>
</Text>
</View>
<View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}>
<IsValidCheck
valid={validCheck.frontLength && validCheck.totalLength}
/>
{!validCheck.totalLength ? (
<Text style={[t.atoms.text]}>
<Trans>May not be longer than 253 characters</Trans>
</Text>
) : (
<Text style={[t.atoms.text, a.text_md]}>
<Trans>Must be at least 3 characters</Trans>
</Text>
)}
</View>
</View>
</View>
</View>
)
}

const styles = StyleSheet.create({
error: {
borderRadius: 6,
marginBottom: 10,
},
})
function IsValidCheck({valid}: {valid: boolean}) {
const t = useTheme()

if (!valid) {
return (
<TimesLarge_Stroke2_Corner0_Rounded
size="md"
style={{color: t.palette.negative_500}}
/>
)
}

return (
<Check_Stroke2_Corner0_Rounded
size="md"
style={{color: t.palette.positive_700}}
/>
)
}
5 changes: 3 additions & 2 deletions src/view/com/auth/create/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {msg} from '@lingui/macro'
import * as EmailValidator from 'email-validator'
import {getAge} from 'lib/strings/time'
import {logger} from '#/logger'
import {createFullHandle} from '#/lib/strings/handles'
import {createFullHandle, validateHandle} from '#/lib/strings/handles'
import {cleanError} from '#/lib/strings/errors'
import {useOnboardingDispatch} from '#/state/shell/onboarding'
import {useSessionApi} from '#/state/session'
Expand Down Expand Up @@ -282,7 +282,8 @@ function compute(state: CreateAccountState): CreateAccountState {
!!state.email &&
!!state.password
} else if (state.step === 2) {
canNext = !!state.handle
canNext =
!!state.handle && validateHandle(state.handle, state.userDomain).overall
} else if (state.step === 3) {
// Step 3 will automatically redirect as soon as the captcha completes
canNext = false
Expand Down

0 comments on commit 817966a

Please sign in to comment.