feat(#2199): add recovery pass popup and change password page

keep-around/11ba998252d14e10552e3bffcce91ab0afdc9329
Andrei Dekterev 4 years ago
parent 6db5a027c1
commit e644249193
  1. 5
      src/features/AuthServiceApp/components/App/index.tsx
  2. 87
      src/features/AuthServiceApp/components/ChangePassword/hooks.tsx
  3. 83
      src/features/AuthServiceApp/components/ChangePassword/index.tsx
  4. 9
      src/features/AuthServiceApp/components/Login/hooks.tsx
  5. 18
      src/features/AuthServiceApp/components/Login/index.tsx
  6. 62
      src/features/AuthServiceApp/components/RecoveryPopup/hooks.tsx
  7. 87
      src/features/AuthServiceApp/components/RecoveryPopup/index.tsx
  8. 147
      src/features/AuthServiceApp/components/RecoveryPopup/styled.tsx
  9. 12
      src/features/AuthServiceApp/config/lexics.tsx
  10. 1
      src/features/AuthServiceApp/config/pages.tsx
  11. 1
      src/features/AuthServiceApp/hooks/useAuthFields.tsx
  12. 38
      src/features/AuthServiceApp/requests/changePassword.tsx
  13. 35
      src/features/AuthServiceApp/requests/loginCheck.tsx
  14. 30
      src/features/AuthServiceApp/styled.tsx

@ -17,6 +17,7 @@ import { lexics } from '../../config/lexics'
const Login = lazy(() => import('../Login'))
const Registration = lazy(() => import('../Registration'))
const ChangePassword = lazy(() => import('../ChangePassword'))
const Main = styled.main`
width: 100%;
@ -44,6 +45,10 @@ export const App = () => {
<Registration />
</Route>
<Route path={PAGES.change_password}>
<ChangePassword />
</Route>
<Redirect to={PAGES.login} />
</Switch>
</Main>

@ -0,0 +1,87 @@
import {
ChangeEvent,
SyntheticEvent,
useState,
} from 'react'
import { useLocation } from 'react-router-dom'
import { useAuthFields } from 'features/AuthServiceApp/hooks/useAuthFields'
import { changePassword } from 'features/AuthServiceApp/requests/changePassword'
export const useChangePasswordForm = () => {
const [error, setError] = useState('')
const [isFetching, setIsFetching] = useState(false)
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const {
checkPassword,
} = useAuthFields('login')
const { search } = useLocation()
const getUrlParam = (key: string) => {
const token = new URLSearchParams(search).get(key)
return token
}
const token = getUrlParam('token')
const handleSubmit = async (event: SyntheticEvent) => {
event?.preventDefault()
setIsFetching(true)
try {
const redirectUrl = await changePassword(password, token || '')
setError('')
setIsFetching(false)
if (redirectUrl) {
await window.location.assign(redirectUrl)
}
} catch (err) {
setError(String(err))
setIsFetching(false)
}
}
const onPasswordChange = ({
target: { value },
}: ChangeEvent<HTMLInputElement>) => {
if (!checkPassword(value)) {
setError('check_password')
} else {
setError('')
}
setPassword(value)
}
const onConfirmPasswordChange = ({
target: { value },
}: ChangeEvent<HTMLInputElement>) => {
setConfirmPassword(value)
if (isMatchPasswords(password, value)) {
setError('')
} else {
setError('error_passwords_missmatch')
}
}
const isMatchPasswords = (pass1: string, pass2: string) => pass1 === pass2
const isSubmitDisabled = !password
|| !confirmPassword
|| !isMatchPasswords(password, confirmPassword)
|| Boolean(error)
|| isFetching
return {
confirmPassword,
error,
handleSubmit,
isFetching,
isSubmitDisabled,
onConfirmPasswordChange,
onPasswordChange,
password,
}
}

@ -0,0 +1,83 @@
import { T9n } from 'features/T9n'
import { ArrowLoader } from 'features/ArrowLoader'
import { PasswordInput } from '../PasswordInput'
import { Logo } from '../Logo'
import { useChangePasswordForm } from './hooks'
import {
BlockTitle,
CenterBlock,
FormText,
InputGroup,
ButtonsBlock,
Form,
ButtonSolid,
Error,
} from '../../styled'
const ChangePassword = () => {
const {
confirmPassword,
error,
// formError,
handleSubmit,
isFetching,
isSubmitDisabled,
// onPasswordBlur,
onConfirmPasswordChange,
onPasswordChange,
password,
} = useChangePasswordForm()
return (
<CenterBlock>
<Logo />
<Form
// method='PUT'
// // ref={formRef}
// // action={url}
onSubmit={handleSubmit}
>
<BlockTitle t='set_new_password' />
<FormText>
<T9n t='set_new_password_for_your_account' />
</FormText>
<InputGroup>
<PasswordInput
type='password'
name='password'
autoComplete='current-password'
placeholderLexic='password_new'
value={password}
onChange={onPasswordChange}
// onBlur={onPasswordBlur}
/>
<PasswordInput
type='password'
name='confirm_password'
autoComplete='current-password'
placeholderLexic='password_repeat'
value={confirmPassword}
onChange={onConfirmPasswordChange}
// onBlur={onPasswordBlur}
/>
</InputGroup>
<Error>
<T9n t={error} />
</Error>
<ButtonsBlock>
<ButtonSolid disabled={isSubmitDisabled}>
{
isFetching
? <ArrowLoader />
: <T9n t='change_password' />
}
</ButtonSolid>
</ButtonsBlock>
</Form>
</CenterBlock>
)
}
export default ChangePassword

@ -19,6 +19,8 @@ export const useLoginForm = () => {
const { lang } = useLexicsStore()
const [authError, setAuthError] = useState('')
const [isFetching, setIsFetching] = useState(false)
const [isModalOpen, setIsModalOpen] = useState(false)
const formRef = useRef<HTMLFormElement>(null)
const {
email,
@ -38,6 +40,10 @@ export const useLoginForm = () => {
|| isFetching
)
const handleModalOpen = () => {
setIsModalOpen(true)
}
const submitForm = () => formRef.current?.submit()
const handleError = (error: string) => {
@ -62,14 +68,17 @@ export const useLoginForm = () => {
email,
formError,
formRef,
handleModalOpen,
handleSubmit,
isFetching,
isModalOpen,
isSubmitDisabled,
onEmailBlur,
onEmailChange,
onPasswordBlur,
onPasswordChange,
password,
setIsModalOpen,
url: addLanguageUrlParam(lang, url),
}
}

@ -1,5 +1,6 @@
import { T9n } from 'features/T9n'
import { ArrowLoader } from 'features/ArrowLoader'
import { RecoveryPopup } from 'features/AuthServiceApp/components/RecoveryPopup'
import { PAGES } from '../../config/pages'
import { LanguageSelect } from '../LanguageSelect'
@ -11,7 +12,9 @@ import {
InputGroup,
BlockTitle,
CenterBlock,
Container,
ButtonsBlock,
ForgotPass,
Form,
ButtonSolid,
Error,
@ -25,14 +28,17 @@ const Login = () => {
email,
formError,
formRef,
handleModalOpen,
handleSubmit,
isFetching,
isModalOpen,
isSubmitDisabled,
onEmailBlur,
onEmailChange,
onPasswordBlur,
onPasswordChange,
password,
setIsModalOpen,
url,
} = useLoginForm()
@ -85,10 +91,16 @@ const Login = () => {
<T9n t='register' />
</RegisterButton>
</ButtonsBlock>
<LanguageSelectWrapper>
<LanguageSelect />
</LanguageSelectWrapper>
<Container>
<ForgotPass onClick={handleModalOpen}>
<T9n t='forgot_password' />
</ForgotPass>
<LanguageSelectWrapper>
<LanguageSelect />
</LanguageSelectWrapper>
</Container>
</Form>
<RecoveryPopup isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} />
</CenterBlock>
)
}

@ -0,0 +1,62 @@
import { ChangeEvent, useState } from 'react'
import { isValidEmail } from 'features/AuthServiceApp/helpers/isValidEmail'
import { loginCheckChangePass } from 'features/AuthServiceApp/requests/loginCheck'
export const useRecovery = (setIsModalOpen: (value: boolean) => void) => {
const [isSendBtnDisabled, setIsSendBtnDisabled] = useState(true)
const [error, setError] = useState('')
const [email, setEmail] = useState('')
const [isFetching, setIsFetching] = useState(false)
const [isSendMessage, setIsSendMessage] = useState(false)
const onEmailChange = ({
target: { value },
}: ChangeEvent<HTMLInputElement>) => {
setError('')
setEmail(value)
if (isValidEmail(value)) {
setIsSendBtnDisabled(false)
} else {
setIsSendBtnDisabled(true)
}
}
const closePopup = () => {
setIsSendMessage(false)
setIsFetching(false)
setError('')
setEmail('')
setIsSendBtnDisabled(true)
setIsModalOpen(false)
}
const handleError = (err: string) => {
setError(err)
setIsFetching(false)
}
const handleSubmit = () => {
if (isSendMessage && !isFetching) {
closePopup()
} else {
setIsFetching(true)
loginCheckChangePass(email)
.then(() => setIsSendMessage(true))
.then(() => setIsFetching(false))
.catch(handleError)
}
}
return {
closePopup,
email,
error,
handleSubmit,
isFetching,
isSendBtnDisabled,
isSendMessage,
// onEmailBlur,
onEmailChange,
}
}

@ -0,0 +1,87 @@
import { Fragment } from 'react'
import { T9n } from 'features/T9n'
import { ArrowLoader } from 'features/ArrowLoader'
import { useRecovery } from './hooks'
import {
Modal,
Wrapper,
Header,
HeaderTitle,
Footer,
ApplyButton,
Text,
Body,
} from './styled'
import { Input } from '../Input'
// import { Error } from '../../styled'
import { InputGroup, Error } from '../../styled'
type Props = {
isModalOpen: boolean,
setIsModalOpen: (value: boolean) => void,
}
export const RecoveryPopup = (props: Props) => {
const { isModalOpen, setIsModalOpen } = props
const {
closePopup,
error,
// onEmailBlur,
handleSubmit,
isFetching,
isSendBtnDisabled,
isSendMessage,
onEmailChange,
} = useRecovery(setIsModalOpen)
return (
<Modal
isOpen={isModalOpen}
withCloseButton
close={closePopup}
>
<Wrapper>
<Header>
<HeaderTitle>
<T9n t='recovery_email' />
</HeaderTitle>
</Header>
<Body>
{isSendMessage ? (
<Text>
<T9n t='send_new_email_for_change_pass' />
</Text>
) : (
<Fragment>
<InputGroup>
<Input
autoFocus
type='email'
name='email'
autoComplete='email'
placeholderLexic='registration_email'
onChange={onEmailChange}
// onBlur={onEmailBlur}
/>
</InputGroup>
<Error>
<T9n t={error} />
</Error>
</Fragment>
)}
<Footer>
<ApplyButton onClick={handleSubmit} disabled={isSendBtnDisabled}>
{(isFetching && <ArrowLoader />)
|| (isSendMessage ? 'Ok' : <T9n t='send' />)}
</ApplyButton>
</Footer>
</Body>
</Wrapper>
</Modal>
)
}

@ -0,0 +1,147 @@
import styled, { css } from 'styled-components/macro'
import { isMobileDevice } from 'config/userAgent'
import { devices } from 'config/devices'
import { ModalWindow } from 'features/Modal/styled'
import { Modal as BaseModal } from 'features/Modal'
import { Header as BaseHeader } from 'features/PopupComponents'
import { ButtonSolid } from 'features/Common'
export const Modal = styled(BaseModal)`
background-color: rgba(0, 0, 0, 0.7);
padding: 0 60px;
${ModalWindow} {
width: 577px;
max-width: 577px;
max-height: 414px;
padding-top: 60px;
background-color: #333333;
border-radius: 5px;
@media (max-width: 1370px) {
width: 70rem;
height: auto;
}
${isMobileDevice
? css`
@media ${devices.mobile}{
height: auto;
top: -7vh;
}
`
: ''};
}
`
type WrapperProps = {
isFetching?: boolean,
}
export const Wrapper = styled.div<WrapperProps>`
${({ isFetching }) => (
isFetching
? css`pointer-events: none;`
: ''
)}
`
export const Header = styled(BaseHeader)`
height: auto;
padding-top: 60;
justify-content: center;
${isMobileDevice
? css`
@media ${devices.mobile}{
padding-top: 33px;
}
`
: ''};
`
export const HeaderTitle = styled.span`
font-weight: 700;
font-size: 24px;
line-height: 24px;
color: #FFFFFF;
${isMobileDevice
? css`
@media ${devices.mobile}{
font-size: 14px;
line-height: 20px;
}
@media (orientation: landscape) {
font-size: 20px;
}
`
: ''};
`
export const Body = styled.div`
padding: 25px 25px 0 25px;
display: flex;
flex-direction: column;
font-weight: normal;
justify-content: center;
font-size: 20px;
line-height: 27px;
${isMobileDevice
? css`
@media ${devices.mobile}{
padding: 13px 25px 0;
flex-direction: column;
}
@media (orientation: landscape){
padding: 22px 23px 0 29px;
}
`
: ''};
`
export const Footer = styled.div`
width: 100%;
display: flex;
justify-content: center;
padding: 1.89rem;
${isMobileDevice
? css`
@media ${devices.mobile}{
flex-direction: column;
padding: 20px 25px 20px;
}
`
: ''};
`
export const ApplyButton = styled(ButtonSolid)`
max-width: 270px;
border-radius: 5px;
font-weight: 600;
height: 50px;
font-size: 20px;
${isMobileDevice
? css`
@media ${devices.mobile}{
width: 100%;
min-height: 42px;
margin-bottom: 20px;
}
@media (orientation: landscape){
width: 290px;
min-height: 42px;
}
`
: ''};
`
export const Text = styled.span`
margin-bottom: 20px;
display: flex;
justify-content: center;
`

@ -1,4 +1,5 @@
export const lexics = {
change_password: 13442,
check_email: 15907,
check_password: 15842,
confirm_2_hours: 15906,
@ -11,19 +12,30 @@ export const lexics = {
error_invalid_email_or_password: 15774,
error_invalid_platform: 15925,
error_missing_required_argument: 15921,
error_passwords_missmatch: 15841,
error_simple_password: 12940,
error_unsupported_response_type: 15922,
error_user_already_created: 15926,
error_user_not_found: 15956,
error_user_not_found_recovery: 1417,
forgot_password: 1561,
form_email: 12912,
form_password: 751,
go_back: 1907,
i_accept: 15737,
login: 13404,
password_new: 15056,
password_repeat: 15057,
recovery_email: 16238,
register: 13328,
registration_email: 16239,
registration_successful: 12945,
send: 16166,
send_confirm: 15905,
send_new_email: 15830,
send_new_email_for_change_pass: 14702,
set_new_password: 16240,
set_new_password_for_your_account: 15840,
sign_up: 1305,
step_title_login: 13404,
step_title_registration: 1306,

@ -1,4 +1,5 @@
export const PAGES = {
change_password: '/change_password',
login: '/authorize',
registration: '/registration',
}

@ -48,6 +48,7 @@ export const useAuthFields = (page: 'login'|'registration') => {
}
return {
checkPassword,
email,
error,
onEmailBlur,

@ -0,0 +1,38 @@
import { getApiUrl } from '../config/routes'
const errorLexics = {
1: 'error_зassword mismatch',
4: 'error_user_not_found',
}
type FailedResponse = {
error: {
code: keyof typeof errorLexics,
message?: string,
},
ok: false,
}
type SuccessResponse = {
data: {
url: string,
},
ok: true,
}
export const changePassword = async (password: string, token: string) => {
const url = getApiUrl(`/change_password?token=${token}`)
const init: RequestInit = {
body: new URLSearchParams({
password,
}),
method: 'PUT',
}
const response = await fetch(url, init)
const body: SuccessResponse | FailedResponse = await response.json()
if (body.ok) return body.data.url
return Promise.reject(errorLexics[body.error.code])
}

@ -0,0 +1,35 @@
import { getApiUrl } from '../config/routes'
const errorLexics = {
4: 'error_user_not_found_recovery',
8: 'error_failed_to_send_email',
}
type FailedResponse = {
error: {
code: keyof typeof errorLexics,
message?: string,
},
ok: false,
}
type SuccessResponse = {
ok: true,
}
export const loginCheckChangePass = async (email: string) => {
const url = getApiUrl('/change_password')
const init: RequestInit = {
body: new URLSearchParams({
email,
}),
method: 'POST',
}
const response = await fetch(url, init)
const body: SuccessResponse | FailedResponse = await response.json()
if (body.ok) return Promise.resolve()
return Promise.reject(errorLexics[body.error.code])
}

@ -45,7 +45,7 @@ export const Form = styled.form`
? css`
width: 100%;
margin-top: 78px;
@media screen and (orientation: landscape){
@media screen and (orientation: landscape) {
margin-bottom: 0;
margin-top: 10px;
padding: 0;
@ -61,7 +61,7 @@ export const BlockTitle = styled(T9n)`
font-size: 24px;
height: 24px;
color: ${({ theme: { colors } }) => colors.text100};
margin-bottom: 30px;
margin-bottom: 30px;,
${isMobileDevice
? css`
font-size: 20px;
@ -120,6 +120,30 @@ export const Error = styled.span`
`
export const LanguageSelectWrapper = styled.div`
margin-top: 15px;
align-self: flex-end;
`
export const ForgotPass = styled.div`
font-size: 16px;
color: #ffffff;
justify-content: flex-start;
width: 100%;
cursor: pointer;
`
export const Container = styled.div`
min-width: 100%;
margin-top: 15px;
display: flex;
flex-direction: row;
align-items: center;
`
export const FormText = styled.span`
justify-content: flex-start;
text-align: center;
margin-bottom: 15px;
font-size: 16px;
color: #ffffff;
width: 100%;
cursor: pointer;
`

Loading…
Cancel
Save