Skip to content

Commit

Permalink
More feedback to user when login fails (#45)
Browse files Browse the repository at this point in the history
* add more feedback to user when login fails

* unrefined animation

* simpler animation

* move Help in comp tree

* version bump

* v bump

* liam styling suggestions
  • Loading branch information
jonmattgray authored Sep 23, 2022
1 parent 326d58f commit 7870c23
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 63 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@digicatapult/morello-ui",
"version": "0.7.5",
"version": "0.7.6",
"description": "User interface for interacting with morello-api",
"main": "src/index.js",
"scripts": {
Expand Down
39 changes: 26 additions & 13 deletions src/components/demos/write/Demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const IconWrapper = styled.div`

const successfulLogin = (apiOutput) =>
extractLoginResult(apiOutput) === 'Login succeeded'
const failedLogin = (apiOutput) =>
extractLoginResult(apiOutput) === 'Login failed'
const loginError = (apiOutput) => extractLoginResult(apiOutput) === 'error'

const SecretDesktop = ({ icons }) => {
Expand Down Expand Up @@ -58,6 +60,7 @@ export default function WriteDemo(props) {
const { theme } = demoState
const isMorello = theme.name === 'Morello'

const [animateLoginFailed, setAnimateLoginFailed] = useState(false)
const [showHelp, setShowHelp] = useState(false)
const [awaitingApi, setAwaitingApi] = useState(false)
const [apiOutput, setApiOutput] = useState('')
Expand Down Expand Up @@ -101,32 +104,42 @@ export default function WriteDemo(props) {
}
}, [usernamePasswordPairs, execute, binaryName, theme])

useEffect(() => {
if (isMorello && (failedLogin(apiOutput) || loginError(apiOutput))) {
setAnimateLoginFailed(true)
setTimeout(() => setAnimateLoginFailed(false), 1000)
}
}, [apiOutput, isMorello, setAnimateLoginFailed])

return (
<>
<Header {...props} showClose={true} />
<Wrapper {...theme.wrapper}>
{successfulLogin(apiOutput) ? (
<SecretDesktop icons={props.secretDesktop} />
) : (
<Box {...demoState}>
<Container
styles={{ height: '100%', paddingTop: '150px' }}
size={10}
>
<LoginForm
demoState={demoState}
showSpinner={awaitingApi}
setUsernamePasswordPairs={setUsernamePasswordPairs}
apiOutput={apiOutput}
/>
</Container>
<>
<Help
theme={theme}
content={helpContent}
showContentState={showHelp}
setShowContentState={setShowHelp}
/>
</Box>
<Box {...demoState} animate={animateLoginFailed}>
<Container
styles={{ height: '100%', paddingTop: '150px' }}
size={10}
>
<LoginForm
demoState={demoState}
showSpinner={awaitingApi}
setUsernamePasswordPairs={setUsernamePasswordPairs}
apiOutput={apiOutput}
setApiOutput={setApiOutput}
/>
</Container>
</Box>
</>
)}
{!isMorello
? successfulLogin(apiOutput) && (
Expand Down
69 changes: 37 additions & 32 deletions src/components/demos/write/LoginForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ import React, { useState, useEffect } from 'react'
import styled from 'styled-components'

import Input from '../../shared/Input'
import { Container, Spinner } from '../../shared/Common'
import { Spinner } from '../../shared/Common'
import { extractLoginResult } from '../../../utils/write-demo-output'

const Button = styled.button((props) => props)
const LoginAttemptText = styled.p((props) => props)
const Form = styled.form`
display: flex;
flex-direction: column;
`

const failedLogin = (apiOutput) =>
extractLoginResult(apiOutput) === 'Login failed'
Expand All @@ -17,6 +21,7 @@ export default function LoginForm({
showSpinner,
setUsernamePasswordPairs,
apiOutput,
setApiOutput,
}) {
const [usernameInput, setUsernameInput] = useState('')
const [passwordInput, setPasswordInput] = useState('')
Expand All @@ -32,6 +37,7 @@ export default function LoginForm({
const passwordAtMaxLength = passwordInput.length >= passwordUpperBound

const enterUsernameAndPassword = async (e) => {
setApiOutput('')
setSomeUsernameTyped(true)
setSomePasswordTyped(true)
e.preventDefault()
Expand Down Expand Up @@ -68,56 +74,55 @@ export default function LoginForm({
}
}, [usernameInput, passwordInput])

useEffect(() => {
if (failedLogin(apiOutput) || loginError(apiOutput)) {
setPasswordInput('')
setSomePasswordTyped(false)
}
}, [apiOutput])

return (
<form onSubmit={enterUsernameAndPassword}>
<Form onSubmit={enterUsernameAndPassword}>
<Input
label={'Username'}
theme={demoState.theme.form}
setInputState={setUsernameInput}
showInputError={usernameAtMaxLength || noUsernameEntered}
InputErrorWarning={UsernameErrorWarning}
cySelector={'username'}
style={{ alignSelf: 'center' }}
/>
<Input
label={'Password'}
theme={demoState.theme.form}
value={passwordInput}
setInputState={setPasswordInput}
upperBound={passwordUpperBound}
inputType={'password'}
showInputError={passwordAtMaxLength || noPasswordEntered}
InputErrorWarning={PasswordErrorWarning}
cySelector={'password'}
style={{ alignSelf: 'center' }}
/>
<Container
size={10}
styles={{
flexDirection: 'column',
alignItems: 'center',
gap: '30px',
}}
<Button
{...demoState.theme.form.loginButton}
data-cy={'login'}
type={'submit'}
disabled={showSpinner}
>
{showSpinner ? <Spinner /> : `Login`}
</Button>
<LoginAttemptText
{...demoState.theme.form.loginAttempt}
visibility={
failedLogin(apiOutput) || loginError(apiOutput) ? 'visible' : 'hidden'
}
data-cy={'login-attempt'}
>
<Button
{...demoState.theme.form.loginButton}
data-cy={'login'}
type={'submit'}
disabled={showSpinner}
>
{showSpinner ? <Spinner /> : `Login`}
</Button>
<LoginAttemptText
{...demoState.theme.form.loginAttempt}
visibility={
failedLogin(apiOutput) || loginError(apiOutput)
? 'visible'
: 'hidden'
}
data-cy={'login-attempt'}
>
{failedLogin(apiOutput) && `Incorrect username or password`}
{loginError(apiOutput) &&
`Suspicious activity detected - account locked`}
</LoginAttemptText>
</Container>
</form>
{failedLogin(apiOutput) && `Incorrect username or password`}
{loginError(apiOutput) &&
`Suspicious activity detected - account locked`}
</LoginAttemptText>
</Form>
)
}
14 changes: 7 additions & 7 deletions src/components/shared/Box.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import React from 'react'
import styled from 'styled-components'

import { Row } from './Common'
import Title from './Title'

const Window = styled.div((props) => props)
const Body = styled.div((props) => props)
import { Window } from './Common'

export default function Box(props) {
const { theme } = props

return (
<Window
data-cy={'content-container'}
{...theme.primary.window}
{...theme.primary.demoWindow}
style={{
...theme.primary.window,
...theme.primary.demoWindow,
animationPlayState: props.animate ? 'running' : 'paused',
}}
>
<Title title={props.windowTitle} theme={theme} />
<Row flex={'auto'}>
<Body {...theme.primary.windowBody}>{props.children}</Body>
<div style={theme.primary.windowBody}>{props.children}</div>
</Row>
</Window>
)
Expand Down
21 changes: 20 additions & 1 deletion src/components/shared/Common.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import styled from 'styled-components'
import styled, { css, keyframes } from 'styled-components'

const params = {
screen: {
Expand Down Expand Up @@ -91,3 +91,22 @@ export const Spinner = styled.div`
border-radius: 50%;
animation: spinner 1.5s linear infinite;
`

const shake = keyframes`
15%,
60% {
transform: translate3d(-5px, 0, 0);
}
40%,
85% {
transform: translate3d(5px, 0, 0);
}
`
const shakeAnimation = css`
${shake} 0.8s linear infinite;
`

export const Window = styled.div`
animation: ${shakeAnimation};
`
5 changes: 2 additions & 3 deletions src/components/shared/Help.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const Wrapper = styled.div`
`

const ContentWrapper = styled.div`
max-width: 500px;
max-height: 400px;
max-width: 20vw;
max-height: 60vh;
padding: 10px;
background: ${(props) => props.background};
display: ${(props) => props.display};
Expand Down Expand Up @@ -44,7 +44,6 @@ export default function Help({
const toggle = () => {
setShowContentState(!showContentState)
}

return (
<Wrapper>
<IconButton
Expand Down
5 changes: 4 additions & 1 deletion src/components/shared/Input.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@ const Warning = styled.p((props) => props)
export default function Input({
label,
theme,
value,
setInputState,
inputType = 'text',
upperBound,
cySelector,
showInputError,
InputErrorWarning,
style,
}) {
return (
<div>
<div style={style}>
<Label {...theme.label}>{label}</Label>
<Row>
<InputBox
{...theme.input}
data-cy={`${cySelector}`}
type={inputType}
maxLength={upperBound}
value={value}
onChange={(e) => {
setInputState(e.target.value)
}}
Expand Down
8 changes: 5 additions & 3 deletions src/fixtures/themes.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export const Themes = (arch) => {
background: '-moz-linear-gradient(top, #e0e0e0, #ffffff)',
borderRadius: '3px',
border: '1px solid #717171',
marginRight: '20px',
}
: {
width: '350px',
Expand Down Expand Up @@ -96,8 +95,9 @@ export const Themes = (arch) => {
},
loginAttempt: {
fontFamily: isCheri ? 'OpenSans' : 'Monaco',
fontSize: '14px',
color: isCheri ? '#000' : '#fff',
fontSize: '18px',
color: '#f00',
alignSelf: 'center',
},
saveSecretButton: {
width: '60px',
Expand All @@ -121,6 +121,8 @@ export const Themes = (arch) => {
justifyContent: 'center',
alignItems: 'center',
display: 'flex',
marginBottom: '20px',
alignSelf: 'center',
},
},
wrapper: {
Expand Down

0 comments on commit 7870c23

Please sign in to comment.