Skip to content

Commit

Permalink
[C-3421] Add email-in-use hint (#6825)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanjeffers authored Dec 1, 2023
1 parent b88999f commit 3d8fbaa
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 44 deletions.
22 changes: 22 additions & 0 deletions packages/harmony/src/components/hint/Hint.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react'

import { IconError } from 'icons'

import { Hint } from './Hint'

const meta: Meta<typeof Hint> = {
title: 'Components/Hint [beta]',
component: Hint
}

export default meta

type Story = StoryObj<typeof Hint>

export const Default: Story = {
args: {
icon: IconError,
children:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vulputate libero et velit interdum, ac aliquet odio'
}
}
32 changes: 32 additions & 0 deletions packages/harmony/src/components/hint/Hint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { IconComponent } from 'components/icon'
import { Paper, PaperProps } from 'components/layout/Paper'
import { Text } from 'components/text'

type HintProps = {
icon: IconComponent
} & PaperProps

/*
* A way of informing the user of important details in line in a prominent way.
*/
export const Hint = (props: HintProps) => {
const { icon: Icon, children, ...other } = props
return (
<Paper
role='alert'
ph='l'
pv='m'
backgroundColor='surface2'
alignItems='center'
gap='l'
shadow='flat'
border='strong'
{...other}
>
<Icon size='l' color='default' />
<Text variant='body' color='default'>
{children}
</Text>
</Paper>
)
}
1 change: 1 addition & 0 deletions packages/harmony/src/components/hint/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Hint'
1 change: 1 addition & 0 deletions packages/harmony/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './button'
export * from './completion-check'
export * from './icon'
export * from './text-link'
export * from './hint'
1 change: 1 addition & 0 deletions packages/harmony/src/components/layout/Paper/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ export type BasePaperProps = {
*/
shadow?: Exclude<ShadowOptions, 'drop'>
}

export type PaperProps = BasePaperProps & FlexProps
10 changes: 10 additions & 0 deletions packages/probers/cypress/e2e/signIn.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ describe('Sign In', () => {
assertOnSignInPage()
})

it.only('can navigate to sign-in after entering email in sign-up', () => {
cy.visit('signup')
cy.findByRole('textbox', { name: /email/i }).type(email)
cy.findByRole('button', { name: /sign up free/i }).click()
cy.findByRole('alert').within(() => {
cy.findByRole('link', { name: /Sign In/ }).click()
})
assertOnSignInPage()
})

it('can sign in', () => {
cy.visit('signin')
assertOnSignInPage()
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/app/AudiusQueryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type AudiusQueryProviderProps = {
children: ReactNode
}

const audiusQueryContext = {
export const audiusQueryContext = {
apiClient,
audiusBackend: audiusBackendInstance,
audiusSdk,
Expand Down
28 changes: 26 additions & 2 deletions packages/web/src/components/form-fields/HarmonyTextField.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { useEffect } from 'react'

import { useDebouncedCallback } from '@audius/common'
import { TextInput, TextInputProps } from '@audius/harmony'
import { useField } from 'formik'
import { useField, useFormikContext } from 'formik'

export type HarmonyTextFieldProps = TextInputProps & {
name: string
Expand All @@ -11,12 +14,33 @@ export type HarmonyTextFieldProps = TextInputProps & {
/** Function to transform the input value upon `onChange`.
* E.g. a function to trim whitespace */
transformValue?: (value: string) => string
debouncedValidationMs?: number
}

// TODO: rename to TextField and replace old usages
export const HarmonyTextField = (props: HarmonyTextFieldProps) => {
const { name, clearErrorOnChange = true, transformValue, ...other } = props
const {
name,
clearErrorOnChange = true,
transformValue,
debouncedValidationMs,
...other
} = props
const [field, { touched, error }, { setError }] = useField(name)
const { value } = field
const { validateField } = useFormikContext()

const debouncedValidateField = useDebouncedCallback(
(field: string) => validateField(field),
[validateField],
500
)

useEffect(() => {
if (debouncedValidationMs) {
debouncedValidateField(name)
}
}, [debouncedValidationMs, debouncedValidateField, name, value])

const hasError = Boolean(touched && error)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Box, useTheme } from '@audius/harmony'
import { IconImage } from '@audius/stems'
import { Box, useTheme, IconImage } from '@audius/harmony'

import {
getCoverPhotoField,
Expand Down
10 changes: 10 additions & 0 deletions packages/web/src/pages/sign-up-page/components/EmailTakenHint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Hint, IconError } from '@audius/harmony'
import { ErrorMessage } from 'formik'

export const EmailTakenHint = () => {
return (
<ErrorMessage name='email'>
{(errorMessage) => <Hint icon={IconError}>{errorMessage}</Hint>}
</ErrorMessage>
)
}
66 changes: 28 additions & 38 deletions packages/web/src/pages/sign-up-page/pages/CreateEmailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { useCallback, useContext } from 'react'
import { useCallback } from 'react'

import { AudiusQueryContext, signUpFetch } from '@audius/common'
import {} from '@audius/common'
import {
Box,
Button,
ButtonType,
Divider,
Flex,
Hint,
IconArrowRight,
IconAudiusLogoHorizontalColor,
IconError,
Text,
TextLink
} from '@audius/harmony'
import { Form, Formik, FormikHelpers } from 'formik'
import { ErrorMessage, Form, Formik } from 'formik'
import { useDispatch, useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { toFormikValidationSchema } from 'zod-formik-adapter'
Expand All @@ -39,6 +41,8 @@ import { SignUpWithMetaMaskButton } from '../components/SignUpWithMetaMaskButton
import { Heading, Page } from '../components/layout'
import { emailSchema } from '../utils/emailSchema'

const EmailSchema = toFormikValidationSchema(emailSchema)

export const messages = {
title: 'Sign Up For Audius',
emailLabel: 'Email',
Expand All @@ -62,14 +66,11 @@ export type SignUpEmailValues = {
email: string
}

const FormSchema = toFormikValidationSchema(emailSchema)

export const CreateEmailPage = () => {
const { isMobile } = useMedia()
const dispatch = useDispatch()
const navigate = useNavigateToPage()
const existingEmailValue = useSelector(getEmailField)
const queryContext = useContext(AudiusQueryContext)

const initialValues = {
email: existingEmailValue.value ?? ''
Expand All @@ -90,42 +91,25 @@ export const CreateEmailPage = () => {
)

const handleSubmit = useCallback(
async (
values: SignUpEmailValues,
{ setErrors }: FormikHelpers<SignUpEmailValues>
) => {
async (values: SignUpEmailValues) => {
const { email } = values
if (queryContext !== null) {
try {
// Check identity API for existing emails
const emailExists = await signUpFetch.isEmailInUse(
{ email },
queryContext
)
// Set the email in the store
dispatch(setValueField('email', values.email))
if (emailExists) {
// Redirect to sign in if the email exists already
navigate(SIGN_IN_PAGE)
} else {
// Move onto the password page
navigate(SIGN_UP_PASSWORD_PAGE)
}
} catch (e) {
// Unknown error state ¯\_(ツ)_/¯
setErrors({ email: messages.unknownError })
}
}
dispatch(setValueField('email', email))
navigate(SIGN_UP_PASSWORD_PAGE)
},
[dispatch, navigate, queryContext]
[dispatch, navigate]
)

const signInLink = (
<TextLink variant='visible' asChild>
<Link to={SIGN_IN_PAGE}>{messages.signIn}</Link>
</TextLink>
)

return (
<Formik
validationSchema={FormSchema}
initialValues={initialValues}
onSubmit={handleSubmit}
validateOnBlur
validationSchema={EmailSchema}
validateOnChange={false}
>
{({ isSubmitting }) => (
Expand Down Expand Up @@ -156,7 +140,16 @@ export const CreateEmailPage = () => {
name='email'
autoComplete='email'
label={messages.emailLabel}
debouncedValidationMs={500}
helperText={null}
/>
<ErrorMessage name='email'>
{(errorMessage) => (
<Hint icon={IconError}>
{errorMessage} {signInLink}
</Hint>
)}
</ErrorMessage>
<Divider>
<Text variant='body' size={isMobile ? 's' : 'm'} color='subdued'>
{messages.socialsDividerText}
Expand All @@ -182,10 +175,7 @@ export const CreateEmailPage = () => {
size={isMobile ? 'm' : 'l'}
css={{ textAlign: isMobile ? 'center' : undefined }}
>
{messages.haveAccount}{' '}
<TextLink variant='visible' asChild>
<Link to={SIGN_IN_PAGE}>{messages.signIn}</Link>
</TextLink>
{messages.haveAccount} {signInLink}
</Text>
</Flex>
{!isMobile ? (
Expand Down
11 changes: 10 additions & 1 deletion packages/web/src/pages/sign-up-page/utils/emailSchema.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { signUpFetch } from '@audius/common'
import { z } from 'zod'

import { audiusQueryContext } from 'app/AudiusQueryProvider'
import { EMAIL_REGEX } from 'utils/email'

const messages = {
invalidEmail: 'Please enter a valid email.'
invalidEmail: 'Please enter a valid email.',
emailInUse: 'Email Already Taken'
}

export const emailSchema = z.object({
email: z
.string({ required_error: messages.invalidEmail })
.regex(EMAIL_REGEX, { message: messages.invalidEmail })
.refine(
async (email) => {
return !(await signUpFetch.isEmailInUse({ email }, audiusQueryContext))
},
{ message: messages.emailInUse }
)
})
1 change: 1 addition & 0 deletions packages/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export default defineConfig(({ mode }) => {
resolve: {
alias: {
// Should be able to use vite-tsconfig-paths instead
app: '/src/app',
assets: '/src/assets',
common: '/src/common',
components: '/src/components',
Expand Down

0 comments on commit 3d8fbaa

Please sign in to comment.