Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions frontend/e2e/tests/invite-test.pw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,12 @@ test.describe('Invite Tests', () => {
await page.goto(inviteLink)
// Wait for the form to load
await waitForElementVisible(byId('firstName'))
await expect(page.getByRole('link', { name: 'Log in', exact: true })).toBeVisible()
await setText(byId('firstName'), 'Bullet')
await setText(byId('lastName'), 'Train')
await setText(byId('email'), inviteEmail)
await setText(byId('password'), PASSWORD)
await waitForElementVisible(byId('signup-btn'))
// Wait for form validation to complete before clicking
await page.waitForTimeout(500)
await expect(page.locator(byId('signup-btn'))).toBeEnabled()
await click(byId('signup-btn'))
log('Change email')
await gotoAccountSettings()
Expand All @@ -62,4 +61,26 @@ test.describe('Invite Tests', () => {
await setText("[name='currentPassword']", PASSWORD)
await click(byId('delete-account'))
});

test('Signup sends users to login when their email already has an account @oss', async ({ page }) => {
const { click, setText, waitForElementVisible } = createHelpers(page);

log('Open signup')
await page.goto('/signup')
await waitForElementVisible(byId('firstName'))
await expect(page.getByRole('link', { name: 'Log in', exact: true })).toBeVisible()

log('Sign up with an email that already has an account')
await setText(byId('firstName'), 'Existing')
await setText(byId('lastName'), 'User')
await setText(byId('email'), E2E_USER)
await setText(byId('password'), PASSWORD)
await expect(page.locator(byId('signup-btn'))).toBeEnabled()
await click(byId('signup-btn'))

log('Sent to login, prefilled, with the reason at the top')
await expect(page).toHaveURL(/\/login/)
await expect(page.getByText('You already have an account')).toBeVisible()
await expect(page.locator(byId('email'))).toHaveValue(E2E_USER)
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React, { ChangeEvent, MouseEvent, useEffect, useState } from 'react'
import React, {
ChangeEvent,
FC,
MouseEvent,
useCallback,
useEffect,
useState,
} from 'react'
import { useHistory, useLocation, withRouter } from 'react-router-dom'
import { GoogleOAuthProvider } from '@react-oauth/google'
import ForgotPasswordModal from 'components/modals/ForgotPasswordModal'
Expand All @@ -9,12 +16,10 @@ import Constants from 'common/constants'
import ErrorMessage from 'components/ErrorMessage'
import Button from 'components/base/forms/Button'
import PasswordRequirements from 'components/PasswordRequirements'
import { informationCircleOutline } from 'ionicons/icons'
import { IonIcon } from '@ionic/react'
import { Icon } from 'components/icons'
import classNames from 'classnames'
import InfoMessage from 'components/InfoMessage'
import OnboardingPage from './OnboardingPage'
import OnboardingPage from 'components/pages/OnboardingPage'
import isFreeEmailDomain from 'common/utils/isFreeEmailDomain'
import InputGroup from 'components/base/forms/InputGroup'
import { Link } from 'react-router-dom'
Expand All @@ -33,6 +38,57 @@ import { useGetBuildVersionQuery } from 'common/services/useBuildVersion'
import { useUTMs } from 'common/useUTMs'
import useSignupExperiment from 'common/useSignupExperiment'

type EmailFieldError = string | string[]
type EmailError = { email?: EmailFieldError } | undefined

// The error object never clears itself, so it only applies while the field still
// holds the address it was about. Case-insensitive to match the API's iexact.
const currentEmailError = (
error: EmailError,
email: string,
submittedEmail: string | null,
isSaving: boolean,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
) =>
!isSaving &&
submittedEmail !== null &&
email.toLowerCase() === submittedEmail.toLowerCase()
? error?.email
: undefined

// Matched on the message because the API sends no error code for this. Raised
// by CustomUserCreateSerializer.validate.
const isEmailTaken = (
Comment thread
talissoncosta marked this conversation as resolved.
error: EmailError,
email: string,
submittedEmail: string | null,
isSaving: boolean,
) => {
const current = currentEmailError(error, email, submittedEmail, isSaving)
const messages = Array.isArray(current) ? current : [current]
return messages.some((message) =>
message?.toLowerCase().includes('already exists'),
)
}

// The banner is only for errors with no field to attach to.
const SIGNUP_FIELDS = ['email', 'first_name', 'last_name', 'password']
const hasFieldError = (error?: Record<string, unknown>) =>
SIGNUP_FIELDS.some((field) => !!error?.[field])

// A component, not an effect in the page, because the error only exists inside
// the provider's render prop.
const RedirectWhenTaken: FC<{ taken: boolean; onTaken: () => void }> = ({
onTaken,
taken,
}) => {
useEffect(() => {
if (taken) {
onTaken()
}
}, [taken, onTaken])
return null
}

const HomePage: React.FC = () => {
const history = useHistory()
const location = useLocation()
Expand All @@ -43,6 +99,10 @@ const HomePage: React.FC = () => {
const [lastName, setLastName] = useState('')
const [marketingConsentGiven] = useState(true)
const [password, setPassword] = useState('')
// Null until a signup has been attempted. /login and /signup are the same
// component, so '' would match a login error that arrived before any signup.
const [submittedEmail, setSubmittedEmail] = useState<string | null>(null)
const [emailAlreadyRegistered, setEmailAlreadyRegistered] = useState(false)

const [samlError, setLocalError] = useState(false)
const [samlLoading, setSamlLoading] = useState(false)
Expand Down Expand Up @@ -171,6 +231,13 @@ const HomePage: React.FC = () => {
const redirect = Utils.fromParam().redirect
? `?redirect=${Utils.fromParam().redirect}`
: ''
// Pushed rather than replaced, so Back returns to the signup form with what
// was typed still in it.
const goToLoginAsRegistered = useCallback(() => {
setEmailAlreadyRegistered(true)
history.push(`/login${redirect}`)
}, [history, redirect])

const currentLocation = `${document.location.pathname}${
document.location.search || ''
}`
Expand Down Expand Up @@ -386,17 +453,27 @@ const HomePage: React.FC = () => {
name='form'
onSubmit={(e) => {
e.preventDefault()
setEmailAlreadyRegistered(false)
login({ email, password })
}}
>
{isInvite && (
<div className='notification flex-row'>
<span className='notification__icon mb-2'>
<IonIcon
icon={informationCircleOutline}
/>
{emailAlreadyRegistered && (
<div className='notification d-flex align-items-center justify-content-center gap-2 mb-3'>
<span className='notification__icon d-flex'>
<Icon name='info-outlined' width={20} />
</span>
<p className='notification__text mb-0'>
You already have an account, log in to
continue
</p>
</div>
)}
{isInvite && !emailAlreadyRegistered && (
<div className='notification d-flex align-items-center justify-content-center gap-2 mb-3'>
<span className='notification__icon d-flex'>
<Icon name='info-outlined' width={20} />
</span>
<p className='notification__text pl-3'>
<p className='notification__text mb-0'>
Log in to accept your invite
</p>
</div>
Expand All @@ -405,9 +482,14 @@ const HomePage: React.FC = () => {
<InputGroup
title='Email Address / Username'
data-test='email'
// Controlled so an address carried from signup
// is visible, not just held in state.
value={email}
inputProps={{
className: 'full-width',
error: error?.email,
error: emailAlreadyRegistered
? undefined
: error?.email,
name: 'email',
}}
onChange={(
Expand Down Expand Up @@ -464,20 +546,21 @@ const HomePage: React.FC = () => {
</Button>
</div>
</fieldset>
{(AccountStore.error || samlError) && (
<div
id='error-alert'
className='mt-3 font-weight-medium'
>
<ErrorMessage
error={
typeof AccountStore.error === 'string'
? AccountStore.error
: 'Please check your details and try again'
}
/>
</div>
)}
{!emailAlreadyRegistered &&
(AccountStore.error || samlError) && (
<div
id='error-alert'
className='mt-3 font-weight-medium'
>
<ErrorMessage
error={
typeof AccountStore.error === 'string'
? AccountStore.error
: 'Please check your details and try again'
}
/>
</div>
)}
</form>
)}
</>
Expand Down Expand Up @@ -525,6 +608,7 @@ const HomePage: React.FC = () => {
const isInvite =
document.location.href.indexOf('invite') !==
-1
setSubmittedEmail(email)
register(
{
email,
Expand All @@ -539,7 +623,16 @@ const HomePage: React.FC = () => {
)
}}
>
{error && (
<RedirectWhenTaken
taken={isEmailTaken(
error,
email,
submittedEmail,
isSaving,
)}
onTaken={goToLoginAsRegistered}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{error && !hasFieldError(error) && (
<Row>
<div
id='error-alert'
Expand All @@ -557,29 +650,13 @@ const HomePage: React.FC = () => {
</Row>
)}
{isInvite && (
<div>
<div className='notification flex-row'>
<span className='notification__icon mb-2'>
<IonIcon
icon={informationCircleOutline}
/>
</span>
<p className='notification__text pl-3'>
Create an account to accept your invite
</p>
</div>
<Row className='justify-content-center'>
Have an account?{' '}
<Button
theme='text'
className='ml-1 fw-bold'
onClick={() => {
window.location.href = `/login${redirect}`
}}
>
Log in
</Button>
</Row>
<div className='notification d-flex align-items-center justify-content-center gap-2 mb-3'>
<span className='notification__icon d-flex'>
<Icon name='info-outlined' width={20} />
</span>
<p className='notification__text mb-0'>
Create an account to accept your invite
</p>
</div>
)}
<fieldset id='details'>
Expand Down Expand Up @@ -623,7 +700,12 @@ const HomePage: React.FC = () => {
inputProps={{
autoComplete: 'on',
className: 'full-width',
error: error && error.email,
error: currentEmailError(
error,
email,
submittedEmail,
isSaving,
),
name: 'email',
}}
onChange={(
Expand Down Expand Up @@ -687,6 +769,16 @@ const HomePage: React.FC = () => {
</form>
)}
</Card>
<Row className='justify-content-center'>
Have an account?{' '}
<Button
theme='text'
className='ml-1 fw-bold'
href={`/login${redirect}`}
>
Log in
</Button>
</Row>
</>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/web/components/pages/home-page/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './HomePage'
2 changes: 1 addition & 1 deletion frontend/web/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react'
import { Route, Switch } from 'react-router-dom'

import App from './components/App' // App Wrapper
import HomePage from './components/pages/HomePage'
import HomePage from './components/pages/home-page'
Comment thread
coderabbitai[bot] marked this conversation as resolved.
import Maintenance from './components/Maintenance'
import CreateOrganisationPage from './components/pages/CreateOrganisationPage'
import CreateEnvironmentPage from './components/pages/CreateEnvironmentPage'
Expand Down
1 change: 0 additions & 1 deletion frontend/web/styles/project/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ label {
align-self: center;
&__icon {
color: $success;
font-size: 2em;
}
&__text {
color: $success;
Expand Down
Loading