Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add handle validation to create account UI (#2959)

* show uiState errors in the box as well

simplify copy

update ui for only letters and numbers

add ui validation to handle selection

* simplify names

* Fix accidental text-node render

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

authored by

Hailey
Paul Frazee
and committed by
GitHub
de9df50a 4771caf2

+145 -34
+29
src/lib/strings/handles.ts
··· 1 + // Regex from the go implementation 2 + // https://github.com/bluesky-social/indigo/blob/main/atproto/syntax/handle.go#L10 3 + const VALIDATE_REGEX = 4 + /^([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])?$/ 5 + 1 6 export function makeValidHandle(str: string): string { 2 7 if (str.length > 20) { 3 8 str = str.slice(0, 20) ··· 19 24 export function sanitizeHandle(handle: string, prefix = ''): string { 20 25 return isInvalidHandle(handle) ? '⚠Invalid Handle' : `${prefix}${handle}` 21 26 } 27 + 28 + export interface IsValidHandle { 29 + handleChars: boolean 30 + frontLength: boolean 31 + totalLength: boolean 32 + overall: boolean 33 + } 34 + 35 + // More checks from https://github.com/bluesky-social/atproto/blob/main/packages/pds/src/handle/index.ts#L72 36 + export function validateHandle(str: string, userDomain: string): IsValidHandle { 37 + const fullHandle = createFullHandle(str, userDomain) 38 + 39 + const results = { 40 + handleChars: 41 + !str || (VALIDATE_REGEX.test(fullHandle) && !str.includes('.')), 42 + frontLength: str.length >= 3, 43 + totalLength: fullHandle.length <= 253, 44 + } 45 + 46 + return { 47 + ...results, 48 + overall: !Object.values(results).includes(false), 49 + } 50 + }
+5 -1
src/view/com/auth/create/CreateAccount.tsx
··· 23 23 import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' 24 24 import {TextLink} from '../../util/Link' 25 25 import {getAgent} from 'state/session' 26 - import {createFullHandle} from 'lib/strings/handles' 26 + import {createFullHandle, validateHandle} from 'lib/strings/handles' 27 27 28 28 export function CreateAccount({onPressBack}: {onPressBack: () => void}) { 29 29 const {screen} = useAnalytics() ··· 78 78 } 79 79 80 80 if (uiState.step === 2) { 81 + if (!validateHandle(uiState.handle, uiState.userDomain).overall) { 82 + return 83 + } 84 + 81 85 uiDispatch({type: 'set-processing', value: true}) 82 86 try { 83 87 const res = await getAgent().resolveHandle({
+108 -31
src/view/com/auth/create/Step2.tsx
··· 1 1 import React from 'react' 2 - import {StyleSheet, View} from 'react-native' 2 + import {View} from 'react-native' 3 3 import {CreateAccountState, CreateAccountDispatch} from './state' 4 4 import {Text} from 'view/com/util/text/Text' 5 5 import {StepHeader} from './StepHeader' 6 6 import {s} from 'lib/styles' 7 7 import {TextInput} from '../util/TextInput' 8 - import {createFullHandle} from 'lib/strings/handles' 8 + import { 9 + createFullHandle, 10 + IsValidHandle, 11 + validateHandle, 12 + } from 'lib/strings/handles' 9 13 import {usePalette} from 'lib/hooks/usePalette' 10 - import {ErrorMessage} from 'view/com/util/error/ErrorMessage' 11 14 import {msg, Trans} from '@lingui/macro' 12 15 import {useLingui} from '@lingui/react' 16 + import {atoms as a, useTheme} from '#/alf' 17 + import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 18 + import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times' 19 + import {useFocusEffect} from '@react-navigation/native' 13 20 14 21 /** STEP 3: Your user handle 15 22 * @field User handle ··· 23 30 }) { 24 31 const pal = usePalette('default') 25 32 const {_} = useLingui() 33 + const t = useTheme() 34 + 35 + const [validCheck, setValidCheck] = React.useState<IsValidHandle>({ 36 + handleChars: false, 37 + frontLength: false, 38 + totalLength: true, 39 + overall: false, 40 + }) 41 + 42 + useFocusEffect( 43 + React.useCallback(() => { 44 + setValidCheck(validateHandle(uiState.handle, uiState.userDomain)) 45 + 46 + // Disabling this, because we only want to run this when we focus the screen 47 + // eslint-disable-next-line react-hooks/exhaustive-deps 48 + }, []), 49 + ) 50 + 51 + const onHandleChange = React.useCallback( 52 + (value: string) => { 53 + if (uiState.error) { 54 + uiDispatch({type: 'set-error', value: ''}) 55 + } 56 + 57 + setValidCheck(validateHandle(value, uiState.userDomain)) 58 + uiDispatch({type: 'set-handle', value}) 59 + }, 60 + [uiDispatch, uiState.error, uiState.userDomain], 61 + ) 62 + 26 63 return ( 27 64 <View> 28 65 <StepHeader uiState={uiState} title={_(msg`Your user handle`)} /> 29 - {uiState.error ? ( 30 - <ErrorMessage message={uiState.error} style={styles.error} /> 31 - ) : undefined} 32 66 <View style={s.pb10}> 33 - <TextInput 34 - testID="handleInput" 35 - icon="at" 36 - placeholder="e.g. alice" 37 - value={uiState.handle} 38 - editable 39 - autoFocus 40 - autoComplete="off" 41 - autoCorrect={false} 42 - onChange={value => uiDispatch({type: 'set-handle', value})} 43 - // TODO: Add explicit text label 44 - accessibilityLabel={_(msg`User handle`)} 45 - accessibilityHint={_(msg`Input your user handle`)} 46 - /> 47 - <Text type="lg" style={[pal.text, s.pl5, s.pt10]}> 48 - <Trans>Your full handle will be</Trans>{' '} 49 - <Text type="lg-bold" style={pal.text}> 50 - @{createFullHandle(uiState.handle, uiState.userDomain)} 67 + <View style={s.mb20}> 68 + <TextInput 69 + testID="handleInput" 70 + icon="at" 71 + placeholder="e.g. alice" 72 + value={uiState.handle} 73 + editable 74 + autoFocus 75 + autoComplete="off" 76 + autoCorrect={false} 77 + onChange={onHandleChange} 78 + // TODO: Add explicit text label 79 + accessibilityLabel={_(msg`User handle`)} 80 + accessibilityHint={_(msg`Input your user handle`)} 81 + /> 82 + <Text type="lg" style={[pal.text, s.pl5, s.pt10]}> 83 + <Trans>Your full handle will be</Trans>{' '} 84 + <Text type="lg-bold" style={pal.text}> 85 + @{createFullHandle(uiState.handle, uiState.userDomain)} 86 + </Text> 51 87 </Text> 52 - </Text> 88 + </View> 89 + <View 90 + style={[ 91 + a.w_full, 92 + a.rounded_sm, 93 + a.border, 94 + a.p_md, 95 + a.gap_sm, 96 + t.atoms.border_contrast_low, 97 + ]}> 98 + {uiState.error ? ( 99 + <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 100 + <IsValidIcon valid={false} /> 101 + <Text style={[t.atoms.text, a.text_md, a.flex]}> 102 + {uiState.error} 103 + </Text> 104 + </View> 105 + ) : undefined} 106 + <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 107 + <IsValidIcon valid={validCheck.handleChars} /> 108 + <Text style={[t.atoms.text, a.text_md, a.flex]}> 109 + <Trans>May only contain letters and numbers</Trans> 110 + </Text> 111 + </View> 112 + <View style={[a.w_full, a.flex_row, a.align_center, a.gap_sm]}> 113 + <IsValidIcon 114 + valid={validCheck.frontLength && validCheck.totalLength} 115 + /> 116 + {!validCheck.totalLength ? ( 117 + <Text style={[t.atoms.text]}> 118 + <Trans>May not be longer than 253 characters</Trans> 119 + </Text> 120 + ) : ( 121 + <Text style={[t.atoms.text, a.text_md]}> 122 + <Trans>Must be at least 3 characters</Trans> 123 + </Text> 124 + )} 125 + </View> 126 + </View> 53 127 </View> 54 128 </View> 55 129 ) 56 130 } 57 131 58 - const styles = StyleSheet.create({ 59 - error: { 60 - borderRadius: 6, 61 - marginBottom: 10, 62 - }, 63 - }) 132 + function IsValidIcon({valid}: {valid: boolean}) { 133 + const t = useTheme() 134 + 135 + if (!valid) { 136 + return <Check size="md" style={{color: t.palette.negative_500}} /> 137 + } 138 + 139 + return <Times size="md" style={{color: t.palette.positive_700}} /> 140 + }
+3 -2
src/view/com/auth/create/state.ts
··· 8 8 import * as EmailValidator from 'email-validator' 9 9 import {getAge} from 'lib/strings/time' 10 10 import {logger} from '#/logger' 11 - import {createFullHandle} from '#/lib/strings/handles' 11 + import {createFullHandle, validateHandle} from '#/lib/strings/handles' 12 12 import {cleanError} from '#/lib/strings/errors' 13 13 import {useOnboardingDispatch} from '#/state/shell/onboarding' 14 14 import {useSessionApi} from '#/state/session' ··· 282 282 !!state.email && 283 283 !!state.password 284 284 } else if (state.step === 2) { 285 - canNext = !!state.handle 285 + canNext = 286 + !!state.handle && validateHandle(state.handle, state.userDomain).overall 286 287 } else if (state.step === 3) { 287 288 // Step 3 will automatically redirect as soon as the captcha completes 288 289 canNext = false