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>
This commit is contained in:
Hailey
2024-09-20 14:10:34 -07:00
committed by GitHub
parent e07f5d5980
commit c88b555410
6 changed files with 143 additions and 9 deletions
+1
View File
@@ -205,6 +205,7 @@
"statsig-react-native-expo": "^4.6.1", "statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tlds": "^1.234.0", "tlds": "^1.234.0",
"tldts": "^6.1.46",
"zeego": "^1.6.2", "zeego": "^1.6.2",
"zod": "^3.20.2" "zod": "^3.20.2"
}, },
+82
View File
@@ -0,0 +1,82 @@
import {describe, expect, it} from '@jest/globals'
import tldts from 'tldts'
import {isEmailMaybeInvalid} from '#/lib/strings/email'
describe('emailTypoChecker', () => {
const invalidCases = [
'gnail.com',
'gnail.co',
'gmaill.com',
'gmaill.co',
'gmai.com',
'gmai.co',
'gmal.com',
'gmal.co',
'gmail.co',
'iclod.com',
'iclod.co',
'outllok.com',
'outllok.co',
'outlook.co',
'yaoo.com',
'yaoo.co',
'yaho.com',
'yaho.co',
'yahooo.com',
'yahooo.co',
'yahoo.co',
'hithere.jul',
'agpowj.notshop',
'thisisnot.avalid.tld.nope',
// old tld for czechoslovakia
'czechoslovakia.cs',
// tlds that cbs was registering in 2024 but cancelled
'liveon.cbs',
'its.showtime',
]
const validCases = [
'gmail.com',
// subdomains (tests end of string)
'gnail.com.test.com',
'outlook.com',
'yahoo.com',
'icloud.com',
'firefox.com',
'firefox.co',
'hello.world.com',
'buy.me.a.coffee.shop',
'mayotte.yt',
'aland.ax',
'bouvet.bv',
'uk.gb',
'chad.td',
'somalia.so',
'plane.aero',
'cute.cat',
'together.coop',
'findme.jobs',
'nightatthe.museum',
'industrial.mil',
'czechrepublic.cz',
'lovakia.sk',
// new gtlds in 2024
'whatsinyour.locker',
'letsmakea.deal',
'skeet.now',
'everyone.みんな',
'bourgeois.lifestyle',
'california.living',
'skeet.ing',
'listeningto.music',
'createa.meme',
]
it.each(invalidCases)(`should be invalid: abcde@%s`, domain => {
expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(true)
})
it.each(validCases)(`should be valid: abcde@%s`, domain => {
expect(isEmailMaybeInvalid(`abcde@${domain}`, tldts)).toEqual(false)
})
})
+9
View File
@@ -0,0 +1,9 @@
import type tldts from 'tldts'
const COMMON_ERROR_PATTERN =
/([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))$/
export function isEmailMaybeInvalid(email: string, dynamicTldts: typeof tldts) {
const isIcann = dynamicTldts.parse(email).isIcann
return !isIcann || COMMON_ERROR_PATTERN.test(email)
}
+3 -1
View File
@@ -15,6 +15,7 @@ export interface BackNextButtonsProps {
onBackPress: () => void onBackPress: () => void
onNextPress?: () => void onNextPress?: () => void
onRetryPress?: () => void onRetryPress?: () => void
overrideNextText?: string
} }
export function BackNextButtons({ export function BackNextButtons({
@@ -25,6 +26,7 @@ export function BackNextButtons({
onBackPress, onBackPress,
onNextPress, onNextPress,
onRetryPress, onRetryPress,
overrideNextText,
}: BackNextButtonsProps) { }: BackNextButtonsProps) {
const {_} = useLingui() const {_} = useLingui()
@@ -63,7 +65,7 @@ export function BackNextButtons({
disabled={isLoading || isNextDisabled} disabled={isLoading || isNextDisabled}
onPress={onNextPress}> onPress={onNextPress}>
<ButtonText> <ButtonText>
<Trans>Next</Trans> {overrideNextText ? overrideNextText : <Trans>Next</Trans>}
</ButtonText> </ButtonText>
{isLoading && <ButtonIcon icon={Loader} />} {isLoading && <ButtonIcon icon={Loader} />}
</Button> </Button>
+36 -8
View File
@@ -3,9 +3,11 @@ 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 * as EmailValidator from 'email-validator' import * as EmailValidator from 'email-validator'
import type tldts from 'tldts'
import {logEvent} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isEmailMaybeInvalid} from 'lib/strings/email'
import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {ScreenTransition} from '#/screens/Login/ScreenTransition'
import {is13, is18, useSignupContext} from '#/screens/Signup/state' import {is13, is18, useSignupContext} from '#/screens/Signup/state'
import {Policies} from '#/screens/Signup/StepInfo/Policies' import {Policies} from '#/screens/Signup/StepInfo/Policies'
@@ -46,13 +48,41 @@ export function StepInfo({
const inviteCodeValueRef = useRef<string>(state.inviteCode) const inviteCodeValueRef = useRef<string>(state.inviteCode)
const emailValueRef = useRef<string>(state.email) const emailValueRef = useRef<string>(state.email)
const prevEmailValueRef = useRef<string>(state.email)
const passwordValueRef = useRef<string>(state.password) const passwordValueRef = useRef<string>(state.password)
const onNextPress = React.useCallback(async () => { const [hasWarnedEmail, setHasWarnedEmail] = React.useState<boolean>(false)
const tldtsRef = React.useRef<typeof tldts>()
React.useEffect(() => {
// @ts-expect-error - valid path
import('tldts/dist/index.cjs.min.js').then(tldts => {
tldtsRef.current = tldts
})
}, [])
const onNextPress = () => {
const inviteCode = inviteCodeValueRef.current const inviteCode = inviteCodeValueRef.current
const email = emailValueRef.current const email = emailValueRef.current
const emailChanged = prevEmailValueRef.current !== email
const password = passwordValueRef.current const password = passwordValueRef.current
if (emailChanged && tldtsRef.current) {
if (isEmailMaybeInvalid(email, tldtsRef.current)) {
prevEmailValueRef.current = email
setHasWarnedEmail(true)
return dispatch({
type: 'setError',
value: _(
msg`It looks like you may have entered your email address incorrectly. Are you sure it's right?`,
),
})
}
} else if (hasWarnedEmail) {
setHasWarnedEmail(false)
}
prevEmailValueRef.current = email
if (!is13(state.dateOfBirth)) { if (!is13(state.dateOfBirth)) {
return return
} }
@@ -89,13 +119,7 @@ export function StepInfo({
logEvent('signup:nextPressed', { logEvent('signup:nextPressed', {
activeStep: state.activeStep, activeStep: state.activeStep,
}) })
}, [ }
_,
dispatch,
state.activeStep,
state.dateOfBirth,
state.serviceDescription?.inviteCodeRequired,
])
return ( return (
<ScreenTransition> <ScreenTransition>
@@ -148,6 +172,9 @@ export function StepInfo({
testID="emailInput" testID="emailInput"
onChangeText={value => { onChangeText={value => {
emailValueRef.current = value.trim() emailValueRef.current = value.trim()
if (hasWarnedEmail) {
setHasWarnedEmail(false)
}
}} }}
label={_(msg`Enter your email address`)} label={_(msg`Enter your email address`)}
defaultValue={state.email} defaultValue={state.email}
@@ -208,6 +235,7 @@ export function StepInfo({
onBackPress={onPressBack} onBackPress={onPressBack}
onNextPress={onNextPress} onNextPress={onNextPress}
onRetryPress={refetchServer} onRetryPress={refetchServer}
overrideNextText={hasWarnedEmail ? _(msg`It's correct`) : undefined}
/> />
</ScreenTransition> </ScreenTransition>
) )
+12
View File
@@ -21243,6 +21243,18 @@ tlds@^1.234.0:
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.242.0.tgz#da136a9c95b0efa1a4cd57dca8ef240c08ada4b7" resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.242.0.tgz#da136a9c95b0efa1a4cd57dca8ef240c08ada4b7"
integrity sha512-aP3dXawgmbfU94mA32CJGHmJUE1E58HCB1KmlKRhBNtqBL27mSQcAEmcaMaQ1Za9kIVvOdbxJD3U5ycDy7nJ3w== integrity sha512-aP3dXawgmbfU94mA32CJGHmJUE1E58HCB1KmlKRhBNtqBL27mSQcAEmcaMaQ1Za9kIVvOdbxJD3U5ycDy7nJ3w==
tldts-core@^6.1.46:
version "6.1.46"
resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.46.tgz#062d64981ee83f934f875c178a97e42bcd13bef7"
integrity sha512-zA3ai/j4aFcmbqTvTONkSBuWs0Q4X4tJxa0gV9sp6kDbq5dAhQDSg0WUkReEm0fBAKAGNj+wPKCCsR8MYOYmwA==
tldts@^6.1.46:
version "6.1.46"
resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.46.tgz#0c3c4157efe732caeddd06eee6da891b26bd8a75"
integrity sha512-fw81lXV2CijkNrZAZvee7wegs+EOlTyIuVl/z4q6OUzZHQ1jGL2xQzKXq9geYf/1tzo9LZQLrkcko2m8HLh+rg==
dependencies:
tldts-core "^6.1.46"
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"