about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-09-20 14:10:34 -0700
committerGitHub <noreply@github.com>2024-09-20 16:10:34 -0500
commitc88b555410e7eb6f9ded4648bd6236c9f653c731 (patch)
tree5f963c743aec144223b86d5a2120c95daf7cd441 /src
parente07f5d5980e7647e78bcaf10c7a239725b08ce3d (diff)
downloadvoidsky-c88b555410e7eb6f9ded4648bd6236c9f653c731.tar.zst
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>
Diffstat (limited to 'src')
-rw-r--r--src/lib/strings/__tests__/email.test.ts82
-rw-r--r--src/lib/strings/email.ts9
-rw-r--r--src/screens/Signup/BackNextButtons.tsx4
-rw-r--r--src/screens/Signup/StepInfo/index.tsx44
4 files changed, 130 insertions, 9 deletions
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>
   )