diff options
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/lib/strings/__tests__/email.test.ts | 82 | ||||
-rw-r--r-- | src/lib/strings/email.ts | 9 | ||||
-rw-r--r-- | src/screens/Signup/BackNextButtons.tsx | 4 | ||||
-rw-r--r-- | src/screens/Signup/StepInfo/index.tsx | 44 | ||||
-rw-r--r-- | yarn.lock | 12 |
6 files changed, 143 insertions, 9 deletions
diff --git a/package.json b/package.json index b2356eb75..05b5f086d 100644 --- a/package.json +++ b/package.json @@ -205,6 +205,7 @@ "statsig-react-native-expo": "^4.6.1", "tippy.js": "^6.3.7", "tlds": "^1.234.0", + "tldts": "^6.1.46", "zeego": "^1.6.2", "zod": "^3.20.2" }, diff --git a/src/lib/strings/__tests__/email.test.ts b/src/lib/strings/__tests__/email.test.ts new file mode 100644 index 000000000..4dfda658f --- /dev/null +++ b/src/lib/strings/__tests__/email.test.ts @@ -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) + }) +}) diff --git a/src/lib/strings/email.ts b/src/lib/strings/email.ts new file mode 100644 index 000000000..04b603847 --- /dev/null +++ b/src/lib/strings/email.ts @@ -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) +} diff --git a/src/screens/Signup/BackNextButtons.tsx b/src/screens/Signup/BackNextButtons.tsx index 47256bf6f..e2401bb11 100644 --- a/src/screens/Signup/BackNextButtons.tsx +++ b/src/screens/Signup/BackNextButtons.tsx @@ -15,6 +15,7 @@ export interface BackNextButtonsProps { onBackPress: () => void onNextPress?: () => void onRetryPress?: () => void + overrideNextText?: string } export function BackNextButtons({ @@ -25,6 +26,7 @@ export function BackNextButtons({ onBackPress, onNextPress, onRetryPress, + overrideNextText, }: BackNextButtonsProps) { const {_} = useLingui() @@ -63,7 +65,7 @@ export function BackNextButtons({ disabled={isLoading || isNextDisabled} onPress={onNextPress}> <ButtonText> - <Trans>Next</Trans> + {overrideNextText ? overrideNextText : <Trans>Next</Trans>} </ButtonText> {isLoading && <ButtonIcon icon={Loader} />} </Button> diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx index e0a7912fd..2cdb4b722 100644 --- a/src/screens/Signup/StepInfo/index.tsx +++ b/src/screens/Signup/StepInfo/index.tsx @@ -3,9 +3,11 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import * as EmailValidator from 'email-validator' +import type tldts from 'tldts' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' +import {isEmailMaybeInvalid} from 'lib/strings/email' import {ScreenTransition} from '#/screens/Login/ScreenTransition' import {is13, is18, useSignupContext} from '#/screens/Signup/state' import {Policies} from '#/screens/Signup/StepInfo/Policies' @@ -46,13 +48,41 @@ export function StepInfo({ const inviteCodeValueRef = useRef<string>(state.inviteCode) const emailValueRef = useRef<string>(state.email) + const prevEmailValueRef = useRef<string>(state.email) 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 email = emailValueRef.current + const emailChanged = prevEmailValueRef.current !== email 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)) { return } @@ -89,13 +119,7 @@ export function StepInfo({ logEvent('signup:nextPressed', { activeStep: state.activeStep, }) - }, [ - _, - dispatch, - state.activeStep, - state.dateOfBirth, - state.serviceDescription?.inviteCodeRequired, - ]) + } return ( <ScreenTransition> @@ -148,6 +172,9 @@ export function StepInfo({ testID="emailInput" onChangeText={value => { emailValueRef.current = value.trim() + if (hasWarnedEmail) { + setHasWarnedEmail(false) + } }} label={_(msg`Enter your email address`)} defaultValue={state.email} @@ -208,6 +235,7 @@ export function StepInfo({ onBackPress={onPressBack} onNextPress={onNextPress} onRetryPress={refetchServer} + overrideNextText={hasWarnedEmail ? _(msg`It's correct`) : undefined} /> </ScreenTransition> ) diff --git a/yarn.lock b/yarn.lock index 380e7e4f1..d199d5869 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21243,6 +21243,18 @@ tlds@^1.234.0: resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.242.0.tgz#da136a9c95b0efa1a4cd57dca8ef240c08ada4b7" 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: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" |