diff --git a/package.json b/package.json
index b2356eb75e..05b5f086db 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 0000000000..4dfda658f7
--- /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 0000000000..04b6038476
--- /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 47256bf6f0..e2401bb116 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}>
- Next
+ {overrideNextText ? overrideNextText : Next}
{isLoading && }
diff --git a/src/screens/Signup/StepInfo/index.tsx b/src/screens/Signup/StepInfo/index.tsx
index e0a7912fd7..2cdb4b7224 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(state.inviteCode)
const emailValueRef = useRef(state.email)
+ const prevEmailValueRef = useRef(state.email)
const passwordValueRef = useRef(state.password)
- const onNextPress = React.useCallback(async () => {
+ const [hasWarnedEmail, setHasWarnedEmail] = React.useState(false)
+
+ const tldtsRef = React.useRef()
+ 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 (
@@ -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}
/>
)
diff --git a/yarn.lock b/yarn.lock
index 380e7e4f16..d199d5869f 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"