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:
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user