An ATproto social media client -- with an independent Appview.

Validate TLD in signup (#5426)

* add lib

* add validation

* log

* add some common typos

* add tests

* reset hasWarned state on edit

* shorten path

* Move test file, adjust regex, add test

* Get real nit picky

---------

Co-authored-by: Eric Bailey <git@esb.lol>

authored by hailey.at

Eric Bailey and committed by
GitHub
c88b5554 e07f5d59

+143 -9
+1
package.json
··· 205 205 "statsig-react-native-expo": "^4.6.1", 206 206 "tippy.js": "^6.3.7", 207 207 "tlds": "^1.234.0", 208 + "tldts": "^6.1.46", 208 209 "zeego": "^1.6.2", 209 210 "zod": "^3.20.2" 210 211 },
+82
src/lib/strings/__tests__/email.test.ts
··· 1 + import {describe, expect, it} from '@jest/globals' 2 + import tldts from 'tldts' 3 + 4 + import {isEmailMaybeInvalid} from '#/lib/strings/email' 5 + 6 + describe('emailTypoChecker', () => { 7 + const invalidCases = [ 8 + 'gnail.com', 9 + 'gnail.co', 10 + 'gmaill.com', 11 + 'gmaill.co', 12 + 'gmai.com', 13 + 'gmai.co', 14 + 'gmal.com', 15 + 'gmal.co', 16 + 'gmail.co', 17 + 'iclod.com', 18 + 'iclod.co', 19 + 'outllok.com', 20 + 'outllok.co', 21 + 'outlook.co', 22 + 'yaoo.com', 23 + 'yaoo.co', 24 + 'yaho.com', 25 + 'yaho.co', 26 + 'yahooo.com', 27 + 'yahooo.co', 28 + 'yahoo.co', 29 + 'hithere.jul', 30 + 'agpowj.notshop', 31 + 'thisisnot.avalid.tld.nope', 32 + // old tld for czechoslovakia 33 + 'czechoslovakia.cs', 34 + // tlds that cbs was registering in 2024 but cancelled 35 + 'liveon.cbs', 36 + 'its.showtime', 37 + ] 38 + const validCases = [ 39 + 'gmail.com', 40 + // subdomains (tests end of string) 41 + 'gnail.com.test.com', 42 + 'outlook.com', 43 + 'yahoo.com', 44 + 'icloud.com', 45 + 'firefox.com', 46 + 'firefox.co', 47 + 'hello.world.com', 48 + 'buy.me.a.coffee.shop', 49 + 'mayotte.yt', 50 + 'aland.ax', 51 + 'bouvet.bv', 52 + 'uk.gb', 53 + 'chad.td', 54 + 'somalia.so', 55 + 'plane.aero', 56 + 'cute.cat', 57 + 'together.coop', 58 + 'findme.jobs', 59 + 'nightatthe.museum', 60 + 'industrial.mil', 61 + 'czechrepublic.cz', 62 + 'lovakia.sk', 63 + // new gtlds in 2024 64 + 'whatsinyour.locker', 65 + 'letsmakea.deal', 66 + 'skeet.now', 67 + 'everyone.みんな', 68 + 'bourgeois.lifestyle', 69 + 'california.living', 70 + 'skeet.ing', 71 + 'listeningto.music', 72 + 'createa.meme', 73 + ] 74 + 75 + it.each(invalidCases)(`should be invalid: abcde@%s`, domain => { 76 + expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(true) 77 + }) 78 + 79 + it.each(validCases)(`should be valid: abcde@%s`, domain => { 80 + expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(false) 81 + }) 82 + })
+9
src/lib/strings/email.ts
··· 1 + import type tldts from 'tldts' 2 + 3 + const COMMON_ERROR_PATTERN = 4 + /([a-zA-Z0-9._%+-]+)@(gnail\.(co|com)|gmaill\.(co|com)|gmai\.(co|com)|gmail\.co|gmal\.(co|com)|iclod\.(co|com)|icloud\.co|outllok\.(co|com)|outlok\.(co|com)|outlook\.co|yaoo\.(co|com)|yaho\.(co|com)|yahoo\.co|yahooo\.(co|com))$/ 5 + 6 + export function isEmailMaybeInvalid(email: string, dynamicTldts: typeof tldts) { 7 + const isIcann = dynamicTldts.parse(email).isIcann 8 + return !isIcann || COMMON_ERROR_PATTERN.test(email) 9 + }
+3 -1
src/screens/Signup/BackNextButtons.tsx
··· 15 15 onBackPress: () => void 16 16 onNextPress?: () => void 17 17 onRetryPress?: () => void 18 + overrideNextText?: string 18 19 } 19 20 20 21 export function BackNextButtons({ ··· 25 26 onBackPress, 26 27 onNextPress, 27 28 onRetryPress, 29 + overrideNextText, 28 30 }: BackNextButtonsProps) { 29 31 const {_} = useLingui() 30 32 ··· 63 65 disabled={isLoading || isNextDisabled} 64 66 onPress={onNextPress}> 65 67 <ButtonText> 66 - <Trans>Next</Trans> 68 + {overrideNextText ? overrideNextText : <Trans>Next</Trans>} 67 69 </ButtonText> 68 70 {isLoading && <ButtonIcon icon={Loader} />} 69 71 </Button>
+36 -8
src/screens/Signup/StepInfo/index.tsx
··· 3 3 import {msg, Trans} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import * as EmailValidator from 'email-validator' 6 + import type tldts from 'tldts' 6 7 7 8 import {logEvent} from '#/lib/statsig/statsig' 8 9 import {logger} from '#/logger' 10 + import {isEmailMaybeInvalid} from 'lib/strings/email' 9 11 import {ScreenTransition} from '#/screens/Login/ScreenTransition' 10 12 import {is13, is18, useSignupContext} from '#/screens/Signup/state' 11 13 import {Policies} from '#/screens/Signup/StepInfo/Policies' ··· 46 48 47 49 const inviteCodeValueRef = useRef<string>(state.inviteCode) 48 50 const emailValueRef = useRef<string>(state.email) 51 + const prevEmailValueRef = useRef<string>(state.email) 49 52 const passwordValueRef = useRef<string>(state.password) 50 53 51 - const onNextPress = React.useCallback(async () => { 54 + const [hasWarnedEmail, setHasWarnedEmail] = React.useState<boolean>(false) 55 + 56 + const tldtsRef = React.useRef<typeof tldts>() 57 + React.useEffect(() => { 58 + // @ts-expect-error - valid path 59 + import('tldts/dist/index.cjs.min.js').then(tldts => { 60 + tldtsRef.current = tldts 61 + }) 62 + }, []) 63 + 64 + const onNextPress = () => { 52 65 const inviteCode = inviteCodeValueRef.current 53 66 const email = emailValueRef.current 67 + const emailChanged = prevEmailValueRef.current !== email 54 68 const password = passwordValueRef.current 55 69 70 + if (emailChanged && tldtsRef.current) { 71 + if (isEmailMaybeInvalid(email, tldtsRef.current)) { 72 + prevEmailValueRef.current = email 73 + setHasWarnedEmail(true) 74 + return dispatch({ 75 + type: 'setError', 76 + value: _( 77 + msg`It looks like you may have entered your email address incorrectly. Are you sure it's right?`, 78 + ), 79 + }) 80 + } 81 + } else if (hasWarnedEmail) { 82 + setHasWarnedEmail(false) 83 + } 84 + prevEmailValueRef.current = email 85 + 56 86 if (!is13(state.dateOfBirth)) { 57 87 return 58 88 } ··· 89 119 logEvent('signup:nextPressed', { 90 120 activeStep: state.activeStep, 91 121 }) 92 - }, [ 93 - _, 94 - dispatch, 95 - state.activeStep, 96 - state.dateOfBirth, 97 - state.serviceDescription?.inviteCodeRequired, 98 - ]) 122 + } 99 123 100 124 return ( 101 125 <ScreenTransition> ··· 148 172 testID="emailInput" 149 173 onChangeText={value => { 150 174 emailValueRef.current = value.trim() 175 + if (hasWarnedEmail) { 176 + setHasWarnedEmail(false) 177 + } 151 178 }} 152 179 label={_(msg`Enter your email address`)} 153 180 defaultValue={state.email} ··· 208 235 onBackPress={onPressBack} 209 236 onNextPress={onNextPress} 210 237 onRetryPress={refetchServer} 238 + overrideNextText={hasWarnedEmail ? _(msg`It's correct`) : undefined} 211 239 /> 212 240 </ScreenTransition> 213 241 )
+12
yarn.lock
··· 21243 21243 resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.242.0.tgz#da136a9c95b0efa1a4cd57dca8ef240c08ada4b7" 21244 21244 integrity sha512-aP3dXawgmbfU94mA32CJGHmJUE1E58HCB1KmlKRhBNtqBL27mSQcAEmcaMaQ1Za9kIVvOdbxJD3U5ycDy7nJ3w== 21245 21245 21246 + tldts-core@^6.1.46: 21247 + version "6.1.46" 21248 + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.46.tgz#062d64981ee83f934f875c178a97e42bcd13bef7" 21249 + integrity sha512-zA3ai/j4aFcmbqTvTONkSBuWs0Q4X4tJxa0gV9sp6kDbq5dAhQDSg0WUkReEm0fBAKAGNj+wPKCCsR8MYOYmwA== 21250 + 21251 + tldts@^6.1.46: 21252 + version "6.1.46" 21253 + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.46.tgz#0c3c4157efe732caeddd06eee6da891b26bd8a75" 21254 + integrity sha512-fw81lXV2CijkNrZAZvee7wegs+EOlTyIuVl/z4q6OUzZHQ1jGL2xQzKXq9geYf/1tzo9LZQLrkcko2m8HLh+rg== 21255 + dependencies: 21256 + tldts-core "^6.1.46" 21257 + 21246 21258 tmp@^0.0.33: 21247 21259 version "0.0.33" 21248 21260 resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"